mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
feat(mcp): granular OAuth scopes and per-client rate limiting
- Split `media:read` into `geo:read` and `weather:read` scopes - Add dedicated `atlas:read/write` scopes (previously under `places`) - Add dedicated `todos:read/write` scopes (previously under `collab`) - Rate limiting now keyed by userId+clientId instead of userId alone - Bind MCP sessions to the OAuth client that created them - Log MCP tool calls to audit log with clientId - Invalidate all MCP sessions on addon state change - Reduce session sweep interval from 10min to 1min - Update all translations with new scope labels
This commit is contained in:
@@ -7,38 +7,50 @@ export interface ScopeInfo {
|
||||
group: string
|
||||
}
|
||||
|
||||
export const SCOPE_GROUPS: Record<string, ScopeInfo> = {
|
||||
'trips:read': { label: 'View trips & itineraries', description: 'Read trips, days, day notes, and members', group: 'Trips' },
|
||||
'trips:write': { label: 'Edit trips & itineraries', description: 'Create and update trips, days, notes, and manage members', group: 'Trips' },
|
||||
'trips:delete': { label: 'Delete trips', description: 'Permanently delete entire trips — this action is irreversible', group: 'Trips' },
|
||||
'trips:share': { label: 'Manage share links', description: 'Create, update, and revoke public share links for trips', group: 'Trips' },
|
||||
'places:read': { label: 'View places & map data', description: 'Read places, day assignments, tags, categories, and visited countries', group: 'Places' },
|
||||
'places:write': { label: 'Manage places', description: 'Create, update, and delete places, assignments, tags, and atlas entries', group: 'Places' },
|
||||
'packing:read': { label: 'View packing lists', description: 'Read packing items, bags, and category assignees', group: 'Packing' },
|
||||
'packing:write': { label: 'Manage packing lists', description: 'Add, update, delete, toggle, and reorder packing items and bags', group: 'Packing' },
|
||||
'budget:read': { label: 'View budget', description: 'Read budget items and expense breakdown', group: 'Budget' },
|
||||
'budget:write': { label: 'Manage budget', description: 'Create, update, and delete budget items', group: 'Budget' },
|
||||
'reservations:read': { label: 'View reservations', description: 'Read reservations and accommodation details', group: 'Reservations' },
|
||||
'reservations:write': { label: 'Manage reservations', description: 'Create, update, delete, and reorder reservations', group: 'Reservations' },
|
||||
'collab:read': { label: 'View collaboration', description: 'Read collab notes, polls, messages, and to-do items', group: 'Collaboration' },
|
||||
'collab:write': { label: 'Manage collaboration', description: 'Create, update, and delete collab notes, todos, polls, and messages', group: 'Collaboration' },
|
||||
'notifications:read': { label: 'View notifications', description: 'Read in-app notifications and unread counts', group: 'Notifications' },
|
||||
'notifications:write': { label: 'Manage notifications', description: 'Mark notifications as read and respond to them', group: 'Notifications' },
|
||||
'vacay:read': { label: 'View vacation plans', description: 'Read vacation planning data, entries, and stats', group: 'Vacation' },
|
||||
'vacay:write': { label: 'Manage vacation plans', description: 'Create and manage vacation entries, holidays, and team plans', group: 'Vacation' },
|
||||
'media:read': { label: 'Maps & weather data', description: 'Search locations, resolve map URLs, and fetch weather forecasts', group: 'Media' },
|
||||
export interface ScopeKeys {
|
||||
labelKey: string
|
||||
descriptionKey: string
|
||||
groupKey: string
|
||||
}
|
||||
|
||||
export const SCOPE_GROUPS: Record<string, ScopeKeys> = {
|
||||
'trips:read': { labelKey: 'oauth.scope.trips:read.label', descriptionKey: 'oauth.scope.trips:read.description', groupKey: 'oauth.scope.group.trips' },
|
||||
'trips:write': { labelKey: 'oauth.scope.trips:write.label', descriptionKey: 'oauth.scope.trips:write.description', groupKey: 'oauth.scope.group.trips' },
|
||||
'trips:delete': { labelKey: 'oauth.scope.trips:delete.label', descriptionKey: 'oauth.scope.trips:delete.description', groupKey: 'oauth.scope.group.trips' },
|
||||
'trips:share': { labelKey: 'oauth.scope.trips:share.label', descriptionKey: 'oauth.scope.trips:share.description', groupKey: 'oauth.scope.group.trips' },
|
||||
'places:read': { labelKey: 'oauth.scope.places:read.label', descriptionKey: 'oauth.scope.places:read.description', groupKey: 'oauth.scope.group.places' },
|
||||
'places:write': { labelKey: 'oauth.scope.places:write.label', descriptionKey: 'oauth.scope.places:write.description', groupKey: 'oauth.scope.group.places' },
|
||||
'atlas:read': { labelKey: 'oauth.scope.atlas:read.label', descriptionKey: 'oauth.scope.atlas:read.description', groupKey: 'oauth.scope.group.atlas' },
|
||||
'atlas:write': { labelKey: 'oauth.scope.atlas:write.label', descriptionKey: 'oauth.scope.atlas:write.description', groupKey: 'oauth.scope.group.atlas' },
|
||||
'packing:read': { labelKey: 'oauth.scope.packing:read.label', descriptionKey: 'oauth.scope.packing:read.description', groupKey: 'oauth.scope.group.packing' },
|
||||
'packing:write': { labelKey: 'oauth.scope.packing:write.label', descriptionKey: 'oauth.scope.packing:write.description', groupKey: 'oauth.scope.group.packing' },
|
||||
'todos:read': { labelKey: 'oauth.scope.todos:read.label', descriptionKey: 'oauth.scope.todos:read.description', groupKey: 'oauth.scope.group.todos' },
|
||||
'todos:write': { labelKey: 'oauth.scope.todos:write.label', descriptionKey: 'oauth.scope.todos:write.description', groupKey: 'oauth.scope.group.todos' },
|
||||
'budget:read': { labelKey: 'oauth.scope.budget:read.label', descriptionKey: 'oauth.scope.budget:read.description', groupKey: 'oauth.scope.group.budget' },
|
||||
'budget:write': { labelKey: 'oauth.scope.budget:write.label', descriptionKey: 'oauth.scope.budget:write.description', groupKey: 'oauth.scope.group.budget' },
|
||||
'reservations:read': { labelKey: 'oauth.scope.reservations:read.label', descriptionKey: 'oauth.scope.reservations:read.description', groupKey: 'oauth.scope.group.reservations' },
|
||||
'reservations:write': { labelKey: 'oauth.scope.reservations:write.label', descriptionKey: 'oauth.scope.reservations:write.description', groupKey: 'oauth.scope.group.reservations' },
|
||||
'collab:read': { labelKey: 'oauth.scope.collab:read.label', descriptionKey: 'oauth.scope.collab:read.description', groupKey: 'oauth.scope.group.collab' },
|
||||
'collab:write': { labelKey: 'oauth.scope.collab:write.label', descriptionKey: 'oauth.scope.collab:write.description', groupKey: 'oauth.scope.group.collab' },
|
||||
'notifications:read': { labelKey: 'oauth.scope.notifications:read.label', descriptionKey: 'oauth.scope.notifications:read.description', groupKey: 'oauth.scope.group.notifications' },
|
||||
'notifications:write': { labelKey: 'oauth.scope.notifications:write.label', descriptionKey: 'oauth.scope.notifications:write.description', groupKey: 'oauth.scope.group.notifications' },
|
||||
'vacay:read': { labelKey: 'oauth.scope.vacay:read.label', descriptionKey: 'oauth.scope.vacay:read.description', groupKey: 'oauth.scope.group.vacay' },
|
||||
'vacay:write': { labelKey: 'oauth.scope.vacay:write.label', descriptionKey: 'oauth.scope.vacay:write.description', groupKey: 'oauth.scope.group.vacay' },
|
||||
'geo:read': { labelKey: 'oauth.scope.geo:read.label', descriptionKey: 'oauth.scope.geo:read.description', groupKey: 'oauth.scope.group.geo' },
|
||||
'weather:read': { labelKey: 'oauth.scope.weather:read.label', descriptionKey: 'oauth.scope.weather:read.description', groupKey: 'oauth.scope.group.weather' },
|
||||
}
|
||||
|
||||
export const ALL_SCOPES = Object.keys(SCOPE_GROUPS)
|
||||
|
||||
// Group all scopes for the client registration form
|
||||
export const SCOPE_GROUP_NAMES = [...new Set(Object.values(SCOPE_GROUPS).map(s => s.group))]
|
||||
export const SCOPE_GROUP_NAMES = [...new Set(Object.values(SCOPE_GROUPS).map(s => s.groupKey))]
|
||||
|
||||
export function getScopesByGroup(): Record<string, Array<{ scope: string } & ScopeInfo>> {
|
||||
export function getScopesByGroup(t: (key: string) => string): Record<string, Array<{ scope: string } & ScopeInfo>> {
|
||||
const groups: Record<string, Array<{ scope: string } & ScopeInfo>> = {}
|
||||
for (const [scope, info] of Object.entries(SCOPE_GROUPS)) {
|
||||
if (!groups[info.group]) groups[info.group] = []
|
||||
groups[info.group].push({ scope, ...info })
|
||||
for (const [scope, keys] of Object.entries(SCOPE_GROUPS)) {
|
||||
const group = t(keys.groupKey)
|
||||
if (!groups[group]) groups[group] = []
|
||||
groups[group].push({ scope, label: t(keys.labelKey), description: t(keys.descriptionKey), group })
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
@@ -370,6 +370,11 @@ export default function CollabChat({ tripId, currentUser }: CollabChatProps) {
|
||||
const [showEmoji, setShowEmoji] = useState(false)
|
||||
const [reactMenu, setReactMenu] = useState(null) // { msgId, x, y }
|
||||
const [deletingIds, setDeletingIds] = useState(new Set())
|
||||
const deleteTimersRef = useRef<ReturnType<typeof setTimeout>[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
return () => { deleteTimersRef.current.forEach(clearTimeout) }
|
||||
}, [])
|
||||
|
||||
const containerRef = useRef(null)
|
||||
const messagesRef = useRef(messages)
|
||||
@@ -483,13 +488,14 @@ export default function CollabChat({ tripId, currentUser }: CollabChatProps) {
|
||||
requestAnimationFrame(() => {
|
||||
setDeletingIds(prev => new Set(prev).add(msgId))
|
||||
})
|
||||
setTimeout(async () => {
|
||||
const t = setTimeout(async () => {
|
||||
try {
|
||||
await collabApi.deleteMessage(tripId, msgId)
|
||||
setMessages(prev => prev.map(m => m.id === msgId ? { ...m, _deleted: true } : m))
|
||||
} catch {}
|
||||
setDeletingIds(prev => { const s = new Set(prev); s.delete(msgId); return s })
|
||||
}, 400)
|
||||
deleteTimersRef.current.push(t)
|
||||
}, [tripId])
|
||||
|
||||
const handleReact = useCallback(async (msgId, emoji) => {
|
||||
|
||||
@@ -16,12 +16,13 @@ function formatTime(timeStr, is12h) {
|
||||
}
|
||||
|
||||
function formatDayLabel(date, t, locale) {
|
||||
const d = new Date(date + 'T00:00:00')
|
||||
const now = new Date()
|
||||
const tomorrow = new Date(); tomorrow.setDate(now.getDate() + 1)
|
||||
const nowDate = now.toISOString().split('T')[0]
|
||||
const tomorrowUtc = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1))
|
||||
const tomorrowDate = tomorrowUtc.toISOString().split('T')[0]
|
||||
|
||||
if (d.toDateString() === now.toDateString()) return t('collab.whatsNext.today') || 'Today'
|
||||
if (d.toDateString() === tomorrow.toDateString()) return t('collab.whatsNext.tomorrow') || 'Tomorrow'
|
||||
if (date === nowDate) return t('collab.whatsNext.today') || 'Today'
|
||||
if (date === tomorrowDate) return t('collab.whatsNext.tomorrow') || 'Tomorrow'
|
||||
|
||||
return new Date(date + 'T00:00:00Z').toLocaleDateString(locale || undefined, { weekday: 'short', day: 'numeric', month: 'short', timeZone: 'UTC' })
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import React, { useState } from 'react'
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react'
|
||||
import { getScopesByGroup } from '../../api/oauthScopes'
|
||||
import { useTranslation } from '../../i18n'
|
||||
|
||||
interface Props {
|
||||
selected: string[]
|
||||
onChange: (scopes: string[]) => void
|
||||
}
|
||||
|
||||
const scopesByGroup = getScopesByGroup()
|
||||
|
||||
export default function ScopeGroupPicker({ selected, onChange }: Props): React.ReactElement {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState<Record<string, boolean>>({})
|
||||
|
||||
const scopesByGroup = getScopesByGroup(t)
|
||||
const allScopeKeys = Object.values(scopesByGroup).flat().map(s => s.scope)
|
||||
const allSelected = allScopeKeys.every(s => selected.includes(s))
|
||||
|
||||
@@ -23,7 +24,7 @@ export default function ScopeGroupPicker({ selected, onChange }: Props): React.R
|
||||
onClick={() => onChange(allSelected ? [] : allScopeKeys)}
|
||||
className="text-xs px-2 py-0.5 rounded border transition-colors hover:bg-slate-100 dark:hover:bg-slate-700"
|
||||
style={{ borderColor: 'var(--border-primary)', color: 'var(--text-secondary)' }}>
|
||||
{allSelected ? 'Deselect all' : 'Select all'}
|
||||
{allSelected ? t('settings.oauth.modal.deselectAll') : t('settings.oauth.modal.selectAll')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-1 max-h-96 overflow-y-auto pr-1">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Section from './Section'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import { useToast } from '../shared/Toast'
|
||||
import { Trash2, Copy, Terminal, Plus, Check, KeyRound, ChevronDown, ChevronRight, RefreshCw } from 'lucide-react'
|
||||
@@ -131,6 +131,11 @@ export default function IntegrationsTab(): React.ReactElement {
|
||||
const [mcpCreating, setMcpCreating] = useState(false)
|
||||
const [mcpDeleteId, setMcpDeleteId] = useState<number | null>(null)
|
||||
const [copiedKey, setCopiedKey] = useState<string | null>(null)
|
||||
const copyTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
return () => { if (copyTimerRef.current) clearTimeout(copyTimerRef.current) }
|
||||
}, [])
|
||||
|
||||
const mcpEndpoint = `${window.location.origin}/mcp`
|
||||
const mcpJsonConfigOAuth = `{
|
||||
@@ -195,7 +200,8 @@ export default function IntegrationsTab(): React.ReactElement {
|
||||
const handleCopy = (text: string, key: string) => {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
setCopiedKey(key)
|
||||
setTimeout(() => setCopiedKey(null), 2000)
|
||||
if (copyTimerRef.current) clearTimeout(copyTimerRef.current)
|
||||
copyTimerRef.current = setTimeout(() => setCopiedKey(null), 2000)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import Modal from '../shared/Modal'
|
||||
import { tripsApi, authApi, shareApi } from '../../api/client'
|
||||
import { useToast } from '../shared/Toast'
|
||||
@@ -40,6 +40,11 @@ function ShareLinkSection({ tripId, t }: { tripId: number; t: (key: string, para
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [perms, setPerms] = useState({ share_map: true, share_bookings: true, share_packing: false, share_budget: false, share_collab: false })
|
||||
const toast = useToast()
|
||||
const copyTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
return () => { if (copyTimerRef.current) clearTimeout(copyTimerRef.current) }
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
shareApi.getLink(tripId).then(d => {
|
||||
@@ -77,7 +82,8 @@ function ShareLinkSection({ tripId, t }: { tripId: number; t: (key: string, para
|
||||
if (shareUrl) {
|
||||
navigator.clipboard.writeText(shareUrl)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
if (copyTimerRef.current) clearTimeout(copyTimerRef.current)
|
||||
copyTimerRef.current = setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { createContext, useContext, useState, useCallback, useEffect } from 'react'
|
||||
import React, { useState, useCallback, useEffect, useRef } from 'react'
|
||||
import { CheckCircle, XCircle, AlertCircle, Info, X } from 'lucide-react'
|
||||
|
||||
type ToastType = 'success' | 'error' | 'warning' | 'info'
|
||||
@@ -28,18 +28,27 @@ const ICON_COLORS: Record<ToastType, string> = {
|
||||
|
||||
export function ToastContainer() {
|
||||
const [toasts, setToasts] = useState<Toast[]>([])
|
||||
const timersRef = useRef<ReturnType<typeof setTimeout>[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
timersRef.current.forEach(clearTimeout)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const addToast = useCallback((message: string, type: ToastType = 'info', duration: number = 3000) => {
|
||||
const id = ++toastIdCounter
|
||||
setToasts(prev => [...prev, { id, message, type, duration, removing: false }])
|
||||
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
const t1 = setTimeout(() => {
|
||||
setToasts(prev => prev.map(t => t.id === id ? { ...t, removing: true } : t))
|
||||
setTimeout(() => {
|
||||
const t2 = setTimeout(() => {
|
||||
setToasts(prev => prev.filter(t => t.id !== id))
|
||||
}, 400)
|
||||
timersRef.current.push(t2)
|
||||
}, duration)
|
||||
timersRef.current.push(t1)
|
||||
}
|
||||
|
||||
return id
|
||||
@@ -47,9 +56,10 @@ export function ToastContainer() {
|
||||
|
||||
const removeToast = useCallback((id: number) => {
|
||||
setToasts(prev => prev.map(t => t.id === id ? { ...t, removing: true } : t))
|
||||
setTimeout(() => {
|
||||
const t = setTimeout(() => {
|
||||
setToasts(prev => prev.filter(t => t.id !== id))
|
||||
}, 400)
|
||||
timersRef.current.push(t)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -184,9 +184,6 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.notifications.none': 'معطّل',
|
||||
'admin.notifications.email': 'البريد الإلكتروني (SMTP)',
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'أحداث الإشعارات',
|
||||
'admin.notifications.eventsHint': 'اختر الأحداث التي تُفعّل الإشعارات لجميع المستخدمين.',
|
||||
'admin.notifications.configureFirst': 'قم بتكوين إعدادات SMTP أو Webhook أدناه أولاً، ثم قم بتفعيل الأحداث.',
|
||||
'admin.notifications.save': 'حفظ إعدادات الإشعارات',
|
||||
'admin.notifications.saved': 'تم حفظ إعدادات الإشعارات',
|
||||
'admin.notifications.testWebhook': 'إرسال webhook تجريبي',
|
||||
@@ -233,7 +230,7 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'settings.mcp.endpoint': 'نقطة نهاية MCP',
|
||||
'settings.mcp.clientConfig': 'إعداد العميل',
|
||||
'settings.mcp.clientConfigHint': 'استبدل <your_token> برمز API من القائمة أدناه. قد يحتاج مسار npx إلى ضبط وفق نظامك (مثلاً C:\\PROGRA~1\\nodejs\\npx.cmd على Windows).',
|
||||
'settings.mcp.clientConfigHintOAuth': 'Replace <your_client_id> and <your_client_secret> with the credentials shown in the OAuth 2.1 client you created above. mcp-remote will open your browser to complete the authorization the first time you connect. The path to npx may need to be adjusted for your system (e.g. C:\PROGRA~1\nodejs\npx.cmd on Windows).',
|
||||
'settings.mcp.clientConfigHintOAuth': 'استبدل <your_client_id> و<your_client_secret> ببيانات الاعتماد المعروضة في عميل OAuth 2.1 الذي أنشأته أعلاه. سيفتح mcp-remote متصفحك لإتمام التفويض في أول اتصال. قد يحتاج مسار npx إلى تعديل حسب نظامك (مثال: C:\PROGRA~1\nodejs\npx.cmd على Windows).',
|
||||
'settings.mcp.copy': 'نسخ',
|
||||
'settings.mcp.copied': 'تم النسخ!',
|
||||
'settings.mcp.apiTokens': 'رموز API',
|
||||
@@ -255,6 +252,48 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'settings.mcp.toast.createError': 'فشل إنشاء الرمز',
|
||||
'settings.mcp.toast.deleted': 'تم حذف الرمز',
|
||||
'settings.mcp.toast.deleteError': 'فشل حذف الرمز',
|
||||
'settings.mcp.apiTokensDeprecated': 'رموز API قديمة وستُزال في إصدار مستقبلي. يُرجى استخدام عملاء OAuth 2.1 بدلاً منها.',
|
||||
'settings.oauth.clients': 'عملاء OAuth 2.1',
|
||||
'settings.oauth.clientsHint': 'سجّل عملاء OAuth 2.1 للسماح لتطبيقات MCP الخارجية (Claude Web وCursor وغيرها) بالاتصال دون رموز ثابتة.',
|
||||
'settings.oauth.createClient': 'عميل جديد',
|
||||
'settings.oauth.noClients': 'لا يوجد عملاء OAuth مسجلون.',
|
||||
'settings.oauth.clientId': 'معرّف العميل',
|
||||
'settings.oauth.clientSecret': 'سر العميل',
|
||||
'settings.oauth.deleteClient': 'حذف العميل',
|
||||
'settings.oauth.deleteClientMessage': 'سيتم حذف هذا العميل وجميع الجلسات النشطة بشكل دائم. ستفقد أي تطبيق يستخدمه وصوله فوراً.',
|
||||
'settings.oauth.rotateSecret': 'تجديد السر',
|
||||
'settings.oauth.rotateSecretMessage': 'سيتم إنشاء سر عميل جديد وإبطال جميع الجلسات الحالية فوراً. حدّث تطبيقك قبل إغلاق هذا الحوار.',
|
||||
'settings.oauth.rotateSecretConfirm': 'تجديد',
|
||||
'settings.oauth.rotateSecretConfirming': 'جارٍ التجديد…',
|
||||
'settings.oauth.rotateSecretDoneTitle': 'تم إنشاء سر جديد',
|
||||
'settings.oauth.rotateSecretDoneWarning': 'يُعرض هذا السر مرة واحدة فقط. انسخه الآن وحدّث تطبيقك — تم إبطال جميع الجلسات السابقة.',
|
||||
'settings.oauth.activeSessions': 'جلسات OAuth النشطة',
|
||||
'settings.oauth.sessionScopes': 'النطاقات',
|
||||
'settings.oauth.sessionExpires': 'تنتهي',
|
||||
'settings.oauth.revoke': 'إلغاء',
|
||||
'settings.oauth.revokeSession': 'إلغاء الجلسة',
|
||||
'settings.oauth.revokeSessionMessage': 'سيؤدي هذا إلى إلغاء الوصول لهذه الجلسة OAuth فوراً.',
|
||||
'settings.oauth.modal.createTitle': 'تسجيل عميل OAuth',
|
||||
'settings.oauth.modal.presets': 'إعدادات سريعة',
|
||||
'settings.oauth.modal.clientName': 'اسم التطبيق',
|
||||
'settings.oauth.modal.clientNamePlaceholder': 'مثال: Claude Web، تطبيق MCP الخاص بي',
|
||||
'settings.oauth.modal.redirectUris': 'عناوين URI لإعادة التوجيه',
|
||||
'settings.oauth.modal.redirectUrisPlaceholder': 'https://your-app.com/callback\nhttps://your-app.com/auth',
|
||||
'settings.oauth.modal.redirectUrisHint': 'عنوان URI واحد لكل سطر. يُطلب HTTPS (localhost مستثنى). يُطبق تطابق دقيق.',
|
||||
'settings.oauth.modal.scopes': 'النطاقات المسموح بها',
|
||||
'settings.oauth.modal.scopesHint': 'list_trips وget_trip_summary متاحان دائماً — لا يُطلب نطاق. يساعدان الذكاء الاصطناعي في اكتشاف معرّفات الرحلات.',
|
||||
'settings.oauth.modal.selectAll': 'تحديد الكل',
|
||||
'settings.oauth.modal.deselectAll': 'إلغاء تحديد الكل',
|
||||
'settings.oauth.modal.creating': 'جارٍ التسجيل…',
|
||||
'settings.oauth.modal.create': 'تسجيل العميل',
|
||||
'settings.oauth.modal.createdTitle': 'تم تسجيل العميل',
|
||||
'settings.oauth.modal.createdWarning': 'يُعرض سر العميل مرة واحدة فقط. انسخه الآن — لا يمكن استرداده.',
|
||||
'settings.oauth.toast.createError': 'فشل تسجيل عميل OAuth',
|
||||
'settings.oauth.toast.deleted': 'تم حذف عميل OAuth',
|
||||
'settings.oauth.toast.deleteError': 'فشل حذف عميل OAuth',
|
||||
'settings.oauth.toast.revoked': 'تم إلغاء الجلسة',
|
||||
'settings.oauth.toast.revokeError': 'فشل إلغاء الجلسة',
|
||||
'settings.oauth.toast.rotateError': 'فشل تجديد سر العميل',
|
||||
'settings.account': 'الحساب',
|
||||
'settings.about': 'حول',
|
||||
'settings.about.reportBug': 'الإبلاغ عن خطأ',
|
||||
@@ -1022,6 +1061,7 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'budget.totalBudget': 'إجمالي الميزانية',
|
||||
'budget.byCategory': 'حسب الفئة',
|
||||
'budget.editTooltip': 'انقر للتعديل',
|
||||
'budget.linkedToReservation': 'مرتبط بحجز — عدّل الاسم هناك',
|
||||
'budget.confirm.deleteCategory': 'هل تريد حذف الفئة "{name}" مع {count} إدخالات؟',
|
||||
'budget.deleteCategory': 'حذف الفئة',
|
||||
'budget.perPerson': 'لكل شخص',
|
||||
@@ -1122,6 +1162,9 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'packing.template': 'قالب',
|
||||
'packing.templateApplied': 'تمت إضافة {count} عنصر من القالب',
|
||||
'packing.templateError': 'فشل تطبيق القالب',
|
||||
'packing.saveAsTemplate': 'حفظ كقالب',
|
||||
'packing.templateName': 'اسم القالب',
|
||||
'packing.templateSaved': 'تم حفظ قائمة الحقائب كقالب',
|
||||
'packing.bags': 'أمتعة',
|
||||
'packing.noBag': 'غير معيّن',
|
||||
'packing.totalWeight': 'الوزن الإجمالي',
|
||||
@@ -1405,8 +1448,6 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'memories.reviewTitle': 'مراجعة صورك',
|
||||
'memories.reviewHint': 'انقر على الصور لاستبعادها من المشاركة.',
|
||||
'memories.shareCount': 'مشاركة {count} صور',
|
||||
'memories.immichUrl': 'عنوان خادم Immich',
|
||||
'memories.immichApiKey': 'مفتاح API',
|
||||
'memories.testConnection': 'اختبار الاتصال',
|
||||
'memories.testFirst': 'اختبر الاتصال أولاً',
|
||||
'memories.connected': 'متصل',
|
||||
@@ -1703,6 +1744,70 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'notif.generic.text': 'لديك إشعار جديد',
|
||||
'notif.dev.unknown_event.title': '[DEV] حدث غير معروف',
|
||||
'notif.dev.unknown_event.text': 'نوع الحدث "{event}" غير مسجل في EVENT_NOTIFICATION_CONFIG',
|
||||
|
||||
// OAuth scope groups
|
||||
'oauth.scope.group.trips': 'الرحلات',
|
||||
'oauth.scope.group.places': 'الأماكن',
|
||||
'oauth.scope.group.atlas': 'Atlas',
|
||||
'oauth.scope.group.packing': 'الأمتعة',
|
||||
'oauth.scope.group.todos': 'المهام',
|
||||
'oauth.scope.group.budget': 'الميزانية',
|
||||
'oauth.scope.group.reservations': 'الحجوزات',
|
||||
'oauth.scope.group.collab': 'التعاون',
|
||||
'oauth.scope.group.notifications': 'الإشعارات',
|
||||
'oauth.scope.group.vacay': 'الإجازة',
|
||||
'oauth.scope.group.geo': 'Geo',
|
||||
'oauth.scope.group.weather': 'الطقس',
|
||||
|
||||
// OAuth scope labels & descriptions
|
||||
'oauth.scope.trips:read.label': 'عرض الرحلات وخطط السفر',
|
||||
'oauth.scope.trips:read.description': 'قراءة الرحلات والأيام والملاحظات والأعضاء',
|
||||
'oauth.scope.trips:write.label': 'تحرير الرحلات وخطط السفر',
|
||||
'oauth.scope.trips:write.description': 'إنشاء وتحديث الرحلات والأيام والملاحظات وإدارة الأعضاء',
|
||||
'oauth.scope.trips:delete.label': 'حذف الرحلات',
|
||||
'oauth.scope.trips:delete.description': 'حذف الرحلات بأكملها نهائياً — هذا الإجراء لا يمكن التراجع عنه',
|
||||
'oauth.scope.trips:share.label': 'إدارة روابط المشاركة',
|
||||
'oauth.scope.trips:share.description': 'إنشاء روابط مشاركة عامة وتحديثها وإلغاؤها',
|
||||
'oauth.scope.places:read.label': 'عرض الأماكن وبيانات الخريطة',
|
||||
'oauth.scope.places:read.description': 'قراءة الأماكن وتعيينات الأيام والعلامات والفئات',
|
||||
'oauth.scope.places:write.label': 'إدارة الأماكن',
|
||||
'oauth.scope.places:write.description': 'إنشاء وتحديث وحذف الأماكن والتعيينات والعلامات',
|
||||
'oauth.scope.atlas:read.label': 'عرض Atlas',
|
||||
'oauth.scope.atlas:read.description': 'قراءة الدول والمناطق المزارة وقائمة الأمنيات',
|
||||
'oauth.scope.atlas:write.label': 'إدارة Atlas',
|
||||
'oauth.scope.atlas:write.description': 'تعليم الدول والمناطق كمزارة، وإدارة قائمة الأمنيات',
|
||||
'oauth.scope.packing:read.label': 'عرض قوائم الأمتعة',
|
||||
'oauth.scope.packing:read.description': 'قراءة عناصر الأمتعة والحقائب ومُسنَدي الفئات',
|
||||
'oauth.scope.packing:write.label': 'إدارة قوائم الأمتعة',
|
||||
'oauth.scope.packing:write.description': 'إضافة وتحديث وحذف وتبديل وإعادة ترتيب عناصر الأمتعة والحقائب',
|
||||
'oauth.scope.todos:read.label': 'عرض قوائم المهام',
|
||||
'oauth.scope.todos:read.description': 'قراءة مهام الرحلة ومُسنَدي الفئات',
|
||||
'oauth.scope.todos:write.label': 'إدارة قوائم المهام',
|
||||
'oauth.scope.todos:write.description': 'إنشاء وتحديث وتبديل وحذف وإعادة ترتيب المهام',
|
||||
'oauth.scope.budget:read.label': 'عرض الميزانية',
|
||||
'oauth.scope.budget:read.description': 'قراءة بنود الميزانية وتفاصيل النفقات',
|
||||
'oauth.scope.budget:write.label': 'إدارة الميزانية',
|
||||
'oauth.scope.budget:write.description': 'إنشاء وتحديث وحذف بنود الميزانية',
|
||||
'oauth.scope.reservations:read.label': 'عرض الحجوزات',
|
||||
'oauth.scope.reservations:read.description': 'قراءة الحجوزات وتفاصيل الإقامة',
|
||||
'oauth.scope.reservations:write.label': 'إدارة الحجوزات',
|
||||
'oauth.scope.reservations:write.description': 'إنشاء وتحديث وحذف وإعادة ترتيب الحجوزات',
|
||||
'oauth.scope.collab:read.label': 'عرض التعاون',
|
||||
'oauth.scope.collab:read.description': 'قراءة ملاحظات التعاون والاستطلاعات والرسائل',
|
||||
'oauth.scope.collab:write.label': 'إدارة التعاون',
|
||||
'oauth.scope.collab:write.description': 'إنشاء وتحديث وحذف الملاحظات والاستطلاعات والرسائل التعاونية',
|
||||
'oauth.scope.notifications:read.label': 'عرض الإشعارات',
|
||||
'oauth.scope.notifications:read.description': 'قراءة إشعارات التطبيق وأعداد غير المقروءة',
|
||||
'oauth.scope.notifications:write.label': 'إدارة الإشعارات',
|
||||
'oauth.scope.notifications:write.description': 'تعليم الإشعارات كمقروءة والرد عليها',
|
||||
'oauth.scope.vacay:read.label': 'عرض خطط الإجازة',
|
||||
'oauth.scope.vacay:read.description': 'قراءة بيانات تخطيط الإجازة والإدخالات والإحصاءات',
|
||||
'oauth.scope.vacay:write.label': 'إدارة خطط الإجازة',
|
||||
'oauth.scope.vacay:write.description': 'إنشاء وإدارة إدخالات الإجازة والعطلات وخطط الفريق',
|
||||
'oauth.scope.geo:read.label': 'الخرائط والترميز الجغرافي',
|
||||
'oauth.scope.geo:read.description': 'البحث عن مواقع وحل عناوين الخرائط والترميز الجغرافي العكسي للإحداثيات',
|
||||
'oauth.scope.weather:read.label': 'توقعات الطقس',
|
||||
'oauth.scope.weather:read.description': 'جلب توقعات الطقس لمواقع الرحلة وتواريخها',
|
||||
}
|
||||
|
||||
export default ar
|
||||
|
||||
@@ -179,9 +179,6 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.notifications.none': 'Desativado',
|
||||
'admin.notifications.email': 'E-mail (SMTP)',
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'Eventos de notificação',
|
||||
'admin.notifications.eventsHint': 'Escolha quais eventos acionam notificações para todos os usuários.',
|
||||
'admin.notifications.configureFirst': 'Configure primeiro as configurações SMTP ou webhook abaixo, depois ative os eventos.',
|
||||
'admin.notifications.save': 'Salvar configurações de notificação',
|
||||
'admin.notifications.saved': 'Configurações de notificação salvas',
|
||||
'admin.notifications.testWebhook': 'Enviar webhook de teste',
|
||||
@@ -295,7 +292,7 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'settings.mcp.endpoint': 'Endpoint MCP',
|
||||
'settings.mcp.clientConfig': 'Configuração do cliente',
|
||||
'settings.mcp.clientConfigHint': 'Substitua <your_token> por um token de API da lista abaixo. O caminho para o npx pode precisar ser ajustado para o seu sistema (ex.: C:\\PROGRA~1\\nodejs\\npx.cmd no Windows).',
|
||||
'settings.mcp.clientConfigHintOAuth': 'Replace <your_client_id> and <your_client_secret> with the credentials shown in the OAuth 2.1 client you created above. mcp-remote will open your browser to complete the authorization the first time you connect. The path to npx may need to be adjusted for your system (e.g. C:\PROGRA~1\nodejs\npx.cmd on Windows).',
|
||||
'settings.mcp.clientConfigHintOAuth': 'Substitua <your_client_id> e <your_client_secret> pelas credenciais exibidas no cliente OAuth 2.1 criado acima. O mcp-remote abrirá seu navegador para concluir a autorização na primeira conexão. O caminho para o npx pode precisar ser ajustado para seu sistema (ex.: C:\\PROGRA~1\\nodejs\\npx.cmd no Windows).',
|
||||
'settings.mcp.copy': 'Copiar',
|
||||
'settings.mcp.copied': 'Copiado!',
|
||||
'settings.mcp.apiTokens': 'Tokens de API',
|
||||
@@ -317,6 +314,48 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'settings.mcp.toast.createError': 'Falha ao criar token',
|
||||
'settings.mcp.toast.deleted': 'Token excluído',
|
||||
'settings.mcp.toast.deleteError': 'Falha ao excluir token',
|
||||
'settings.mcp.apiTokensDeprecated': 'Os tokens de API estão obsoletos e serão removidos em uma versão futura. Por favor, use Clientes OAuth 2.1.',
|
||||
'settings.oauth.clients': 'Clientes OAuth 2.1',
|
||||
'settings.oauth.clientsHint': 'Registre clientes OAuth 2.1 para permitir que aplicações MCP de terceiros (Claude Web, Cursor, etc.) se conectem sem tokens estáticos.',
|
||||
'settings.oauth.createClient': 'Novo cliente',
|
||||
'settings.oauth.noClients': 'Nenhum cliente OAuth registrado.',
|
||||
'settings.oauth.clientId': 'ID do cliente',
|
||||
'settings.oauth.clientSecret': 'Segredo do cliente',
|
||||
'settings.oauth.deleteClient': 'Excluir cliente',
|
||||
'settings.oauth.deleteClientMessage': 'Este cliente e todas as sessões ativas serão removidos permanentemente. Qualquer aplicação que o utilize perderá o acesso imediatamente.',
|
||||
'settings.oauth.rotateSecret': 'Renovar segredo',
|
||||
'settings.oauth.rotateSecretMessage': 'Um novo segredo de cliente será gerado e todas as sessões existentes serão invalidadas imediatamente. Atualize sua aplicação antes de fechar esta janela.',
|
||||
'settings.oauth.rotateSecretConfirm': 'Renovar',
|
||||
'settings.oauth.rotateSecretConfirming': 'Renovando…',
|
||||
'settings.oauth.rotateSecretDoneTitle': 'Novo segredo gerado',
|
||||
'settings.oauth.rotateSecretDoneWarning': 'Este segredo é exibido apenas uma vez. Copie-o agora e atualize sua aplicação — todas as sessões anteriores foram invalidadas.',
|
||||
'settings.oauth.activeSessions': 'Sessões OAuth ativas',
|
||||
'settings.oauth.sessionScopes': 'Escopos',
|
||||
'settings.oauth.sessionExpires': 'Expira',
|
||||
'settings.oauth.revoke': 'Revogar',
|
||||
'settings.oauth.revokeSession': 'Revogar sessão',
|
||||
'settings.oauth.revokeSessionMessage': 'Isso revogará imediatamente o acesso desta sessão OAuth.',
|
||||
'settings.oauth.modal.createTitle': 'Registrar cliente OAuth',
|
||||
'settings.oauth.modal.presets': 'Configurações rápidas',
|
||||
'settings.oauth.modal.clientName': 'Nome da aplicação',
|
||||
'settings.oauth.modal.clientNamePlaceholder': 'ex.: Claude Web, Meu app MCP',
|
||||
'settings.oauth.modal.redirectUris': 'URIs de redirecionamento',
|
||||
'settings.oauth.modal.redirectUrisPlaceholder': 'https://your-app.com/callback\nhttps://your-app.com/auth',
|
||||
'settings.oauth.modal.redirectUrisHint': 'Uma URI por linha. HTTPS obrigatório (localhost isento). Correspondência exata.',
|
||||
'settings.oauth.modal.scopes': 'Escopos permitidos',
|
||||
'settings.oauth.modal.scopesHint': 'list_trips e get_trip_summary estão sempre disponíveis — sem escopo necessário. Permitem à IA descobrir IDs de viagem.',
|
||||
'settings.oauth.modal.selectAll': 'Selecionar tudo',
|
||||
'settings.oauth.modal.deselectAll': 'Desmarcar tudo',
|
||||
'settings.oauth.modal.creating': 'Registrando…',
|
||||
'settings.oauth.modal.create': 'Registrar cliente',
|
||||
'settings.oauth.modal.createdTitle': 'Cliente registrado',
|
||||
'settings.oauth.modal.createdWarning': 'O segredo do cliente é exibido apenas uma vez. Copie-o agora — não pode ser recuperado.',
|
||||
'settings.oauth.toast.createError': 'Falha ao registrar cliente OAuth',
|
||||
'settings.oauth.toast.deleted': 'Cliente OAuth excluído',
|
||||
'settings.oauth.toast.deleteError': 'Falha ao excluir cliente OAuth',
|
||||
'settings.oauth.toast.revoked': 'Sessão revogada',
|
||||
'settings.oauth.toast.revokeError': 'Falha ao revogar sessão',
|
||||
'settings.oauth.toast.rotateError': 'Falha ao renovar segredo do cliente',
|
||||
'settings.mustChangePassword': 'Você deve alterar sua senha antes de continuar. Defina uma nova senha abaixo.',
|
||||
|
||||
// Login
|
||||
@@ -464,7 +503,7 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.keyValid': 'Conectado',
|
||||
'admin.keyInvalid': 'Inválida',
|
||||
'admin.keySaved': 'Chaves de API salvas',
|
||||
'admin.oidcTitle': 'Single Sign-On (OIDC)',
|
||||
'admin.oidcTitle': 'Login Único (OIDC)',
|
||||
'admin.oidcSubtitle': 'Permitir login via provedores externos como Google, Apple, Authentik ou Keycloak.',
|
||||
'admin.oidcDisplayName': 'Nome exibido',
|
||||
'admin.oidcIssuer': 'URL do emissor',
|
||||
@@ -514,7 +553,7 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.addons.catalog.budget.description': 'Acompanhe despesas e planeje o orçamento da viagem',
|
||||
'admin.addons.catalog.documents.name': 'Documentos',
|
||||
'admin.addons.catalog.documents.description': 'Armazene e gerencie documentos de viagem',
|
||||
'admin.addons.catalog.vacay.name': 'Vacay',
|
||||
'admin.addons.catalog.vacay.name': 'Férias',
|
||||
'admin.addons.catalog.vacay.description': 'Planejador de férias pessoal com visão em calendário',
|
||||
'admin.addons.catalog.atlas.name': 'Atlas',
|
||||
'admin.addons.catalog.atlas.description': 'Mapa mundial com países visitados e estatísticas',
|
||||
@@ -547,7 +586,7 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.weather.requestsDesc': 'Grátis, sem chave de API',
|
||||
'admin.weather.locationHint': 'O clima usa o primeiro lugar com coordenadas de cada dia. Se nenhum lugar estiver atribuído ao dia, qualquer lugar da lista serve como referência.',
|
||||
|
||||
'admin.tabs.audit': 'Audit',
|
||||
'admin.tabs.audit': 'Auditoria',
|
||||
|
||||
'admin.audit.subtitle': 'Eventos sensíveis de segurança e administração (backups, usuários, 2FA, configurações).',
|
||||
'admin.audit.empty': 'Nenhum registro de auditoria.',
|
||||
@@ -991,6 +1030,7 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'budget.totalBudget': 'Orçamento total',
|
||||
'budget.byCategory': 'Por categoria',
|
||||
'budget.editTooltip': 'Clique para editar',
|
||||
'budget.linkedToReservation': 'Vinculado a uma reserva — edite o nome por lá',
|
||||
'budget.confirm.deleteCategory': 'Excluir a categoria "{name}" com {count} lançamento(s)?',
|
||||
'budget.deleteCategory': 'Excluir categoria',
|
||||
'budget.perPerson': 'Por pessoa',
|
||||
@@ -1091,6 +1131,9 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'packing.template': 'Modelo',
|
||||
'packing.templateApplied': '{count} itens adicionados do modelo',
|
||||
'packing.templateError': 'Falha ao aplicar modelo',
|
||||
'packing.saveAsTemplate': 'Salvar como modelo',
|
||||
'packing.templateName': 'Nome do modelo',
|
||||
'packing.templateSaved': 'Lista de bagagem salva como modelo',
|
||||
'packing.bags': 'Malas',
|
||||
'packing.noBag': 'Sem mala',
|
||||
'packing.totalWeight': 'Peso total',
|
||||
@@ -1444,8 +1487,6 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'memories.reviewTitle': 'Revise suas fotos',
|
||||
'memories.reviewHint': 'Clique nas fotos para excluí-las do compartilhamento.',
|
||||
'memories.shareCount': 'Compartilhar {count} fotos',
|
||||
'memories.immichUrl': 'URL do servidor Immich',
|
||||
'memories.immichApiKey': 'Chave da API',
|
||||
'memories.testConnection': 'Testar conexão',
|
||||
'memories.testFirst': 'Teste a conexão primeiro',
|
||||
'memories.connected': 'Conectado',
|
||||
@@ -1698,6 +1739,70 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'notif.generic.text': 'Você tem uma nova notificação',
|
||||
'notif.dev.unknown_event.title': '[DEV] Evento desconhecido',
|
||||
'notif.dev.unknown_event.text': 'O tipo de evento "{event}" não está registrado em EVENT_NOTIFICATION_CONFIG',
|
||||
|
||||
// OAuth scope groups
|
||||
'oauth.scope.group.trips': 'Viagens',
|
||||
'oauth.scope.group.places': 'Locais',
|
||||
'oauth.scope.group.atlas': 'Atlas',
|
||||
'oauth.scope.group.packing': 'Bagagem',
|
||||
'oauth.scope.group.todos': 'Tarefas',
|
||||
'oauth.scope.group.budget': 'Orçamento',
|
||||
'oauth.scope.group.reservations': 'Reservas',
|
||||
'oauth.scope.group.collab': 'Colaboração',
|
||||
'oauth.scope.group.notifications': 'Notificações',
|
||||
'oauth.scope.group.vacay': 'Férias',
|
||||
'oauth.scope.group.geo': 'Geo',
|
||||
'oauth.scope.group.weather': 'Clima',
|
||||
|
||||
// OAuth scope labels & descriptions
|
||||
'oauth.scope.trips:read.label': 'Ver viagens e itinerários',
|
||||
'oauth.scope.trips:read.description': 'Ler viagens, dias, notas e membros',
|
||||
'oauth.scope.trips:write.label': 'Editar viagens e itinerários',
|
||||
'oauth.scope.trips:write.description': 'Criar e atualizar viagens, dias, notas e gerenciar membros',
|
||||
'oauth.scope.trips:delete.label': 'Excluir viagens',
|
||||
'oauth.scope.trips:delete.description': 'Excluir viagens permanentemente — esta ação é irreversível',
|
||||
'oauth.scope.trips:share.label': 'Gerenciar links de compartilhamento',
|
||||
'oauth.scope.trips:share.description': 'Criar, atualizar e revogar links de compartilhamento públicos',
|
||||
'oauth.scope.places:read.label': 'Ver locais e dados do mapa',
|
||||
'oauth.scope.places:read.description': 'Ler locais, atribuições de dias, tags e categorias',
|
||||
'oauth.scope.places:write.label': 'Gerenciar locais',
|
||||
'oauth.scope.places:write.description': 'Criar, atualizar e excluir locais, atribuições e tags',
|
||||
'oauth.scope.atlas:read.label': 'Ver Atlas',
|
||||
'oauth.scope.atlas:read.description': 'Ler países visitados, regiões e lista de desejos',
|
||||
'oauth.scope.atlas:write.label': 'Gerenciar Atlas',
|
||||
'oauth.scope.atlas:write.description': 'Marcar países e regiões como visitados, gerenciar lista de desejos',
|
||||
'oauth.scope.packing:read.label': 'Ver listas de bagagem',
|
||||
'oauth.scope.packing:read.description': 'Ler itens, malas e responsáveis por categoria',
|
||||
'oauth.scope.packing:write.label': 'Gerenciar listas de bagagem',
|
||||
'oauth.scope.packing:write.description': 'Adicionar, atualizar, excluir, marcar e reordenar itens e malas',
|
||||
'oauth.scope.todos:read.label': 'Ver listas de tarefas',
|
||||
'oauth.scope.todos:read.description': 'Ler tarefas da viagem e responsáveis por categoria',
|
||||
'oauth.scope.todos:write.label': 'Gerenciar listas de tarefas',
|
||||
'oauth.scope.todos:write.description': 'Criar, atualizar, marcar, excluir e reordenar tarefas',
|
||||
'oauth.scope.budget:read.label': 'Ver orçamento',
|
||||
'oauth.scope.budget:read.description': 'Ler itens de orçamento e detalhamento de despesas',
|
||||
'oauth.scope.budget:write.label': 'Gerenciar orçamento',
|
||||
'oauth.scope.budget:write.description': 'Criar, atualizar e excluir itens de orçamento',
|
||||
'oauth.scope.reservations:read.label': 'Ver reservas',
|
||||
'oauth.scope.reservations:read.description': 'Ler reservas e detalhes de acomodação',
|
||||
'oauth.scope.reservations:write.label': 'Gerenciar reservas',
|
||||
'oauth.scope.reservations:write.description': 'Criar, atualizar, excluir e reordenar reservas',
|
||||
'oauth.scope.collab:read.label': 'Ver colaboração',
|
||||
'oauth.scope.collab:read.description': 'Ler notas colaborativas, enquetes e mensagens',
|
||||
'oauth.scope.collab:write.label': 'Gerenciar colaboração',
|
||||
'oauth.scope.collab:write.description': 'Criar, atualizar e excluir notas, enquetes e mensagens',
|
||||
'oauth.scope.notifications:read.label': 'Ver notificações',
|
||||
'oauth.scope.notifications:read.description': 'Ler notificações e contagens não lidas',
|
||||
'oauth.scope.notifications:write.label': 'Gerenciar notificações',
|
||||
'oauth.scope.notifications:write.description': 'Marcar notificações como lidas e respondê-las',
|
||||
'oauth.scope.vacay:read.label': 'Ver planos de férias',
|
||||
'oauth.scope.vacay:read.description': 'Ler dados de planejamento de férias, entradas e estatísticas',
|
||||
'oauth.scope.vacay:write.label': 'Gerenciar planos de férias',
|
||||
'oauth.scope.vacay:write.description': 'Criar e gerenciar entradas de férias, feriados e planos de equipe',
|
||||
'oauth.scope.geo:read.label': 'Mapas e geocodificação',
|
||||
'oauth.scope.geo:read.description': 'Pesquisar locais, resolver URLs de mapa e geocodificar coordenadas',
|
||||
'oauth.scope.weather:read.label': 'Previsão do tempo',
|
||||
'oauth.scope.weather:read.description': 'Obter previsão do tempo para locais e datas da viagem',
|
||||
}
|
||||
|
||||
export default br
|
||||
|
||||
@@ -181,7 +181,7 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'settings.mcp.endpoint': 'MCP endpoint',
|
||||
'settings.mcp.clientConfig': 'Konfigurace klienta',
|
||||
'settings.mcp.clientConfigHint': 'Nahraďte <your_token> API tokenem ze seznamu níže. Cestu k npx může být nutné upravit pro váš systém (např. C:\\PROGRA~1\\nodejs\\npx.cmd ve Windows).',
|
||||
'settings.mcp.clientConfigHintOAuth': 'Replace <your_client_id> and <your_client_secret> with the credentials shown in the OAuth 2.1 client you created above. mcp-remote will open your browser to complete the authorization the first time you connect. The path to npx may need to be adjusted for your system (e.g. C:\PROGRA~1\nodejs\npx.cmd on Windows).',
|
||||
'settings.mcp.clientConfigHintOAuth': 'Nahraďte <your_client_id> a <your_client_secret> přihlašovacími údaji ze klienta OAuth 2.1, který jste vytvořili výše. mcp-remote při prvním připojení otevře prohlížeč pro dokončení autorizace. Cestu k npx může být nutné upravit pro váš systém (např. C:\\PROGRA~1\\nodejs\\npx.cmd ve Windows).',
|
||||
'settings.mcp.copy': 'Kopírovat',
|
||||
'settings.mcp.copied': 'Zkopírováno!',
|
||||
'settings.mcp.apiTokens': 'API tokeny',
|
||||
@@ -203,6 +203,48 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'settings.mcp.toast.createError': 'Nepodařilo se vytvořit token',
|
||||
'settings.mcp.toast.deleted': 'Token smazán',
|
||||
'settings.mcp.toast.deleteError': 'Nepodařilo se smazat token',
|
||||
'settings.mcp.apiTokensDeprecated': 'API tokeny jsou zastaralé a budou odstraněny v budoucí verzi. Místo toho použijte klienty OAuth 2.1.',
|
||||
'settings.oauth.clients': 'Klienti OAuth 2.1',
|
||||
'settings.oauth.clientsHint': 'Zaregistrujte klienty OAuth 2.1, aby se aplikace MCP třetích stran (Claude Web, Cursor atd.) mohly připojit bez statických tokenů.',
|
||||
'settings.oauth.createClient': 'Nový klient',
|
||||
'settings.oauth.noClients': 'Žádní klienti OAuth nejsou zaregistrováni.',
|
||||
'settings.oauth.clientId': 'ID klienta',
|
||||
'settings.oauth.clientSecret': 'Tajný klíč klienta',
|
||||
'settings.oauth.deleteClient': 'Smazat klienta',
|
||||
'settings.oauth.deleteClientMessage': 'Tento klient a všechny aktivní relace budou trvale odstraněny. Jakákoliv aplikace, která ho používá, okamžitě ztratí přístup.',
|
||||
'settings.oauth.rotateSecret': 'Obnovit tajný klíč',
|
||||
'settings.oauth.rotateSecretMessage': 'Bude vygenerován nový tajný klíč klienta a všechny stávající relace budou okamžitě zneplatněny. Aktualizujte aplikaci před zavřením tohoto dialogu.',
|
||||
'settings.oauth.rotateSecretConfirm': 'Obnovit',
|
||||
'settings.oauth.rotateSecretConfirming': 'Obnovování…',
|
||||
'settings.oauth.rotateSecretDoneTitle': 'Nový tajný klíč vygenerován',
|
||||
'settings.oauth.rotateSecretDoneWarning': 'Tento tajný klíč se zobrazí pouze jednou. Zkopírujte ho nyní a aktualizujte aplikaci — všechny předchozí relace byly zneplatněny.',
|
||||
'settings.oauth.activeSessions': 'Aktivní relace OAuth',
|
||||
'settings.oauth.sessionScopes': 'Oprávnění',
|
||||
'settings.oauth.sessionExpires': 'Vyprší',
|
||||
'settings.oauth.revoke': 'Odvolat',
|
||||
'settings.oauth.revokeSession': 'Odvolat relaci',
|
||||
'settings.oauth.revokeSessionMessage': 'Tím se okamžitě odvolá přístup pro tuto relaci OAuth.',
|
||||
'settings.oauth.modal.createTitle': 'Zaregistrovat klienta OAuth',
|
||||
'settings.oauth.modal.presets': 'Rychlá nastavení',
|
||||
'settings.oauth.modal.clientName': 'Název aplikace',
|
||||
'settings.oauth.modal.clientNamePlaceholder': 'např. Claude Web, Moje MCP aplikace',
|
||||
'settings.oauth.modal.redirectUris': 'Přesměrovací URI',
|
||||
'settings.oauth.modal.redirectUrisPlaceholder': 'https://your-app.com/callback\nhttps://your-app.com/auth',
|
||||
'settings.oauth.modal.redirectUrisHint': 'Jedno URI na řádek. Vyžadováno HTTPS (localhost vyjmuto). Vyžadována přesná shoda.',
|
||||
'settings.oauth.modal.scopes': 'Povolená oprávnění',
|
||||
'settings.oauth.modal.scopesHint': 'list_trips a get_trip_summary jsou vždy dostupné — bez požadovaného oprávnění. Umožňují AI zjistit potřebná ID výletů.',
|
||||
'settings.oauth.modal.selectAll': 'Vybrat vše',
|
||||
'settings.oauth.modal.deselectAll': 'Zrušit výběr',
|
||||
'settings.oauth.modal.creating': 'Registrování…',
|
||||
'settings.oauth.modal.create': 'Zaregistrovat klienta',
|
||||
'settings.oauth.modal.createdTitle': 'Klient zaregistrován',
|
||||
'settings.oauth.modal.createdWarning': 'Tajný klíč klienta se zobrazí pouze jednou. Zkopírujte ho nyní — nelze ho obnovit.',
|
||||
'settings.oauth.toast.createError': 'Registrace klienta OAuth se nezdařila',
|
||||
'settings.oauth.toast.deleted': 'Klient OAuth smazán',
|
||||
'settings.oauth.toast.deleteError': 'Smazání klienta OAuth se nezdařilo',
|
||||
'settings.oauth.toast.revoked': 'Relace odvolána',
|
||||
'settings.oauth.toast.revokeError': 'Odvolání relace se nezdařilo',
|
||||
'settings.oauth.toast.rotateError': 'Obnovení tajného klíče klienta se nezdařilo',
|
||||
'settings.account': 'Účet',
|
||||
'settings.about': 'O aplikaci',
|
||||
'settings.about.reportBug': 'Nahlásit chybu',
|
||||
@@ -275,9 +317,6 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.notifications.none': 'Vypnuto',
|
||||
'admin.notifications.email': 'E-mail (SMTP)',
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'Události oznámení',
|
||||
'admin.notifications.eventsHint': 'Vyberte, které události spouštějí oznámení pro všechny uživatele.',
|
||||
'admin.notifications.configureFirst': 'Nejprve nakonfigurujte nastavení SMTP nebo webhooku níže, poté povolte události.',
|
||||
'admin.notifications.save': 'Uložit nastavení oznámení',
|
||||
'admin.notifications.saved': 'Nastavení oznámení uloženo',
|
||||
'admin.notifications.testWebhook': 'Odeslat testovací webhook',
|
||||
@@ -1020,6 +1059,7 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'budget.totalBudget': 'Celkový rozpočet',
|
||||
'budget.byCategory': 'Podle kategorie',
|
||||
'budget.editTooltip': 'Klikněte pro úpravu',
|
||||
'budget.linkedToReservation': 'Propojeno s rezervací — název upravte tam',
|
||||
'budget.confirm.deleteCategory': 'Opravdu chcete smazat kategorii „{name}” s {count} položkami?',
|
||||
'budget.deleteCategory': 'Smazat kategorii',
|
||||
'budget.perPerson': 'Na osobu',
|
||||
@@ -1110,7 +1150,7 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'packing.menuCheckAll': 'Označit vše',
|
||||
'packing.menuUncheckAll': 'Odznačit vše',
|
||||
'packing.menuDeleteCat': 'Smazat kategorii',
|
||||
'packing.assignUser': 'Přiřadit uživateli',
|
||||
'packing.assignUser': 'Přiřadit uživatele',
|
||||
'packing.noMembers': 'Žádní členové cesty',
|
||||
'packing.addItem': 'Přidat položku',
|
||||
'packing.addItemPlaceholder': 'Název položky...',
|
||||
@@ -1120,6 +1160,9 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'packing.template': 'Šablona',
|
||||
'packing.templateApplied': '{count} položek přidáno ze šablony',
|
||||
'packing.templateError': 'Šablonu se nepodařilo použít',
|
||||
'packing.saveAsTemplate': 'Uložit jako šablonu',
|
||||
'packing.templateName': 'Název šablony',
|
||||
'packing.templateSaved': 'Seznam balení uložen jako šablona',
|
||||
'packing.bags': 'Zavazadla',
|
||||
'packing.noBag': 'Nepřiřazeno',
|
||||
'packing.totalWeight': 'Celková váha',
|
||||
@@ -1403,8 +1446,6 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'memories.reviewTitle': 'Zkontrolujte své fotky',
|
||||
'memories.reviewHint': 'Klikněte na fotky pro vyloučení ze sdílení.',
|
||||
'memories.shareCount': 'Sdílet {count} fotek',
|
||||
'memories.immichUrl': 'URL serveru Immich',
|
||||
'memories.immichApiKey': 'API klíč',
|
||||
'memories.testConnection': 'Otestovat připojení',
|
||||
'memories.testFirst': 'Nejprve otestujte připojení',
|
||||
'memories.connected': 'Připojeno',
|
||||
@@ -1703,6 +1744,70 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'notif.generic.text': 'Máte nové oznámení',
|
||||
'notif.dev.unknown_event.title': '[DEV] Neznámá událost',
|
||||
'notif.dev.unknown_event.text': 'Typ události "{event}" není registrován v EVENT_NOTIFICATION_CONFIG',
|
||||
|
||||
// OAuth scope groups
|
||||
'oauth.scope.group.trips': 'Výlety',
|
||||
'oauth.scope.group.places': 'Místa',
|
||||
'oauth.scope.group.atlas': 'Atlas',
|
||||
'oauth.scope.group.packing': 'Balení',
|
||||
'oauth.scope.group.todos': 'Úkoly',
|
||||
'oauth.scope.group.budget': 'Rozpočet',
|
||||
'oauth.scope.group.reservations': 'Rezervace',
|
||||
'oauth.scope.group.collab': 'Spolupráce',
|
||||
'oauth.scope.group.notifications': 'Oznámení',
|
||||
'oauth.scope.group.vacay': 'Dovolená',
|
||||
'oauth.scope.group.geo': 'Geo',
|
||||
'oauth.scope.group.weather': 'Počasí',
|
||||
|
||||
// OAuth scope labels & descriptions
|
||||
'oauth.scope.trips:read.label': 'Zobrazit výlety a itineráře',
|
||||
'oauth.scope.trips:read.description': 'Číst výlety, dny, poznámky a členy',
|
||||
'oauth.scope.trips:write.label': 'Upravit výlety a itineráře',
|
||||
'oauth.scope.trips:write.description': 'Vytvářet a aktualizovat výlety, dny, poznámky a spravovat členy',
|
||||
'oauth.scope.trips:delete.label': 'Mazat výlety',
|
||||
'oauth.scope.trips:delete.description': 'Trvale smazat celé výlety — tato akce je nevratná',
|
||||
'oauth.scope.trips:share.label': 'Spravovat sdílené odkazy',
|
||||
'oauth.scope.trips:share.description': 'Vytvářet, aktualizovat a rušit veřejné sdílené odkazy',
|
||||
'oauth.scope.places:read.label': 'Zobrazit místa a mapová data',
|
||||
'oauth.scope.places:read.description': 'Číst místa, denní přiřazení, štítky a kategorie',
|
||||
'oauth.scope.places:write.label': 'Spravovat místa',
|
||||
'oauth.scope.places:write.description': 'Vytvářet, aktualizovat a mazat místa, přiřazení a štítky',
|
||||
'oauth.scope.atlas:read.label': 'Zobrazit Atlas',
|
||||
'oauth.scope.atlas:read.description': 'Číst navštívené země, regiony a seznam přání',
|
||||
'oauth.scope.atlas:write.label': 'Spravovat Atlas',
|
||||
'oauth.scope.atlas:write.description': 'Označovat navštívené země a regiony, spravovat seznam přání',
|
||||
'oauth.scope.packing:read.label': 'Zobrazit seznamy balení',
|
||||
'oauth.scope.packing:read.description': 'Číst položky, tašky a přiřazení kategorií',
|
||||
'oauth.scope.packing:write.label': 'Spravovat seznamy balení',
|
||||
'oauth.scope.packing:write.description': 'Přidávat, aktualizovat, mazat, označovat a řadit položky a tašky',
|
||||
'oauth.scope.todos:read.label': 'Zobrazit seznamy úkolů',
|
||||
'oauth.scope.todos:read.description': 'Číst úkoly výletu a přiřazení kategorií',
|
||||
'oauth.scope.todos:write.label': 'Spravovat seznamy úkolů',
|
||||
'oauth.scope.todos:write.description': 'Vytvářet, aktualizovat, označovat, mazat a řadit úkoly',
|
||||
'oauth.scope.budget:read.label': 'Zobrazit rozpočet',
|
||||
'oauth.scope.budget:read.description': 'Číst položky rozpočtu a přehled výdajů',
|
||||
'oauth.scope.budget:write.label': 'Spravovat rozpočet',
|
||||
'oauth.scope.budget:write.description': 'Vytvářet, aktualizovat a mazat položky rozpočtu',
|
||||
'oauth.scope.reservations:read.label': 'Zobrazit rezervace',
|
||||
'oauth.scope.reservations:read.description': 'Číst rezervace a podrobnosti ubytování',
|
||||
'oauth.scope.reservations:write.label': 'Spravovat rezervace',
|
||||
'oauth.scope.reservations:write.description': 'Vytvářet, aktualizovat, mazat a řadit rezervace',
|
||||
'oauth.scope.collab:read.label': 'Zobrazit spolupráci',
|
||||
'oauth.scope.collab:read.description': 'Číst poznámky, ankety a zprávy spolupráce',
|
||||
'oauth.scope.collab:write.label': 'Spravovat spolupráci',
|
||||
'oauth.scope.collab:write.description': 'Vytvářet, aktualizovat a mazat poznámky, ankety a zprávy',
|
||||
'oauth.scope.notifications:read.label': 'Zobrazit oznámení',
|
||||
'oauth.scope.notifications:read.description': 'Číst oznámení v aplikaci a počty nepřečtených',
|
||||
'oauth.scope.notifications:write.label': 'Spravovat oznámení',
|
||||
'oauth.scope.notifications:write.description': 'Označovat oznámení jako přečtená a reagovat na ně',
|
||||
'oauth.scope.vacay:read.label': 'Zobrazit plány dovolené',
|
||||
'oauth.scope.vacay:read.description': 'Číst data plánování dovolené, záznamy a statistiky',
|
||||
'oauth.scope.vacay:write.label': 'Spravovat plány dovolené',
|
||||
'oauth.scope.vacay:write.description': 'Vytvářet a spravovat záznamy dovolené, svátky a týmové plány',
|
||||
'oauth.scope.geo:read.label': 'Mapy a geokódování',
|
||||
'oauth.scope.geo:read.description': 'Vyhledávat místa, řešit URL map a zpětně geokódovat souřadnice',
|
||||
'oauth.scope.weather:read.label': 'Předpovědi počasí',
|
||||
'oauth.scope.weather:read.description': 'Získávat předpovědi počasí pro místa a data výletu',
|
||||
}
|
||||
|
||||
export default cs
|
||||
|
||||
@@ -179,9 +179,6 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.notifications.none': 'Deaktiviert',
|
||||
'admin.notifications.email': 'E-Mail (SMTP)',
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'Benachrichtigungsereignisse',
|
||||
'admin.notifications.eventsHint': 'Wähle, welche Ereignisse Benachrichtigungen für alle Benutzer auslösen.',
|
||||
'admin.notifications.configureFirst': 'Konfiguriere zuerst die SMTP- oder Webhook-Einstellungen unten, dann aktiviere die Events.',
|
||||
'admin.notifications.save': 'Benachrichtigungseinstellungen speichern',
|
||||
'admin.notifications.saved': 'Benachrichtigungseinstellungen gespeichert',
|
||||
'admin.notifications.testWebhook': 'Test-Webhook senden',
|
||||
@@ -228,7 +225,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'settings.mcp.endpoint': 'MCP-Endpunkt',
|
||||
'settings.mcp.clientConfig': 'Client-Konfiguration',
|
||||
'settings.mcp.clientConfigHint': 'Ersetze <your_token> durch ein API-Token aus der Liste unten. Der Pfad zu npx muss ggf. für dein System angepasst werden (z. B. C:\\PROGRA~1\\nodejs\\npx.cmd unter Windows).',
|
||||
'settings.mcp.clientConfigHintOAuth': 'Replace <your_client_id> and <your_client_secret> with the credentials shown in the OAuth 2.1 client you created above. mcp-remote will open your browser to complete the authorization the first time you connect. The path to npx may need to be adjusted for your system (e.g. C:\PROGRA~1\nodejs\npx.cmd on Windows).',
|
||||
'settings.mcp.clientConfigHintOAuth': 'Ersetze <your_client_id> und <your_client_secret> durch die Zugangsdaten des oben erstellten OAuth 2.1-Clients. mcp-remote öffnet beim ersten Verbindungsaufbau deinen Browser zur Autorisierung. Der Pfad zu npx muss ggf. für dein System angepasst werden (z. B. C:\\PROGRA~1\\nodejs\\npx.cmd unter Windows).',
|
||||
'settings.mcp.copy': 'Kopieren',
|
||||
'settings.mcp.copied': 'Kopiert!',
|
||||
'settings.mcp.apiTokens': 'API-Tokens',
|
||||
@@ -250,6 +247,48 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'settings.mcp.toast.createError': 'Token konnte nicht erstellt werden',
|
||||
'settings.mcp.toast.deleted': 'Token gelöscht',
|
||||
'settings.mcp.toast.deleteError': 'Token konnte nicht gelöscht werden',
|
||||
'settings.mcp.apiTokensDeprecated': 'API-Tokens sind veraltet und werden in einer zukünftigen Version entfernt. Bitte verwende stattdessen OAuth 2.1-Clients.',
|
||||
'settings.oauth.clients': 'OAuth 2.1-Clients',
|
||||
'settings.oauth.clientsHint': 'Registriere OAuth 2.1-Clients, damit externe MCP-Anwendungen (Claude Web, Cursor usw.) sich ohne statische Tokens verbinden können.',
|
||||
'settings.oauth.createClient': 'Neuer Client',
|
||||
'settings.oauth.noClients': 'Keine OAuth-Clients registriert.',
|
||||
'settings.oauth.clientId': 'Client-ID',
|
||||
'settings.oauth.clientSecret': 'Client-Secret',
|
||||
'settings.oauth.deleteClient': 'Client löschen',
|
||||
'settings.oauth.deleteClientMessage': 'Dieser Client und alle aktiven Sessions werden dauerhaft entfernt. Jede Anwendung, die ihn nutzt, verliert sofort den Zugriff.',
|
||||
'settings.oauth.rotateSecret': 'Secret erneuern',
|
||||
'settings.oauth.rotateSecretMessage': 'Ein neues Client-Secret wird generiert und alle bestehenden Sessions werden sofort ungültig. Aktualisiere deine Anwendung, bevor du diesen Dialog schließt.',
|
||||
'settings.oauth.rotateSecretConfirm': 'Erneuern',
|
||||
'settings.oauth.rotateSecretConfirming': 'Wird erneuert…',
|
||||
'settings.oauth.rotateSecretDoneTitle': 'Neues Secret generiert',
|
||||
'settings.oauth.rotateSecretDoneWarning': 'Dieses Secret wird nur einmal angezeigt. Kopiere es jetzt und aktualisiere deine Anwendung — alle vorherigen Sessions wurden ungültig gemacht.',
|
||||
'settings.oauth.activeSessions': 'Aktive OAuth-Sessions',
|
||||
'settings.oauth.sessionScopes': 'Berechtigungen',
|
||||
'settings.oauth.sessionExpires': 'Läuft ab',
|
||||
'settings.oauth.revoke': 'Widerrufen',
|
||||
'settings.oauth.revokeSession': 'Session widerrufen',
|
||||
'settings.oauth.revokeSessionMessage': 'Dadurch wird der Zugriff für diese OAuth-Session sofort widerrufen.',
|
||||
'settings.oauth.modal.createTitle': 'OAuth-Client registrieren',
|
||||
'settings.oauth.modal.presets': 'Schnellvorlagen',
|
||||
'settings.oauth.modal.clientName': 'Anwendungsname',
|
||||
'settings.oauth.modal.clientNamePlaceholder': 'z. B. Claude Web, Meine MCP-App',
|
||||
'settings.oauth.modal.redirectUris': 'Redirect-URIs',
|
||||
'settings.oauth.modal.redirectUrisPlaceholder': 'https://your-app.com/callback\nhttps://your-app.com/auth',
|
||||
'settings.oauth.modal.redirectUrisHint': 'Eine URI pro Zeile. HTTPS erforderlich (localhost ausgenommen). Exakte Übereinstimmung erforderlich.',
|
||||
'settings.oauth.modal.scopes': 'Erlaubte Berechtigungen',
|
||||
'settings.oauth.modal.scopesHint': 'list_trips und get_trip_summary sind immer verfügbar — keine Berechtigung nötig. Sie helfen der KI, Trip-IDs zu ermitteln.',
|
||||
'settings.oauth.modal.selectAll': 'Alle auswählen',
|
||||
'settings.oauth.modal.deselectAll': 'Alle abwählen',
|
||||
'settings.oauth.modal.creating': 'Wird registriert…',
|
||||
'settings.oauth.modal.create': 'Client registrieren',
|
||||
'settings.oauth.modal.createdTitle': 'Client registriert',
|
||||
'settings.oauth.modal.createdWarning': 'Das Client-Secret wird nur einmal angezeigt. Kopiere es jetzt — es kann nicht wiederhergestellt werden.',
|
||||
'settings.oauth.toast.createError': 'OAuth-Client konnte nicht registriert werden',
|
||||
'settings.oauth.toast.deleted': 'OAuth-Client gelöscht',
|
||||
'settings.oauth.toast.deleteError': 'OAuth-Client konnte nicht gelöscht werden',
|
||||
'settings.oauth.toast.revoked': 'Session widerrufen',
|
||||
'settings.oauth.toast.revokeError': 'Session konnte nicht widerrufen werden',
|
||||
'settings.oauth.toast.rotateError': 'Client-Secret konnte nicht erneuert werden',
|
||||
'settings.account': 'Konto',
|
||||
'settings.about': 'Über',
|
||||
'settings.about.reportBug': 'Bug melden',
|
||||
@@ -455,11 +494,11 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.requireMfaHint': 'Benutzer ohne 2FA müssen die Einrichtung unter Einstellungen abschließen, bevor sie die App nutzen können.',
|
||||
'admin.apiKeys': 'API-Schlüssel',
|
||||
'admin.apiKeysHint': 'Optional. Aktiviert erweiterte Ortsdaten wie Fotos und Wetter.',
|
||||
'admin.mapsKey': 'Google Maps API Key',
|
||||
'admin.mapsKey': 'Google Maps API-Schlüssel',
|
||||
'admin.mapsKeyHint': 'Für Ortsuche benötigt. Erstellen unter console.cloud.google.com',
|
||||
'admin.mapsKeyHintLong': 'Ohne API Key wird OpenStreetMap für die Ortssuche genutzt. Mit Google API Key können zusätzlich Bilder, Bewertungen und Öffnungszeiten geladen werden. Erstellen unter console.cloud.google.com.',
|
||||
'admin.recommended': 'Empfohlen',
|
||||
'admin.weatherKey': 'OpenWeatherMap API Key',
|
||||
'admin.weatherKey': 'OpenWeatherMap API-Schlüssel',
|
||||
'admin.weatherKeyHint': 'Für Wetterdaten. Kostenlos unter openweathermap.org',
|
||||
'admin.validateKey': 'Test',
|
||||
'admin.keyValid': 'Verbunden',
|
||||
@@ -527,7 +566,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.addons.subtitleAfter': ' nach deinen Wünschen anzupassen.',
|
||||
'admin.addons.enabled': 'Aktiviert',
|
||||
'admin.addons.disabled': 'Deaktiviert',
|
||||
'admin.addons.type.trip': 'Trip',
|
||||
'admin.addons.type.trip': 'Reise',
|
||||
'admin.addons.type.global': 'Global',
|
||||
'admin.addons.type.integration': 'Integration',
|
||||
'admin.addons.tripHint': 'Verfügbar als Tab innerhalb jedes Trips',
|
||||
@@ -731,7 +770,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'atlas.addToBucketHint': 'Als Wunschziel speichern',
|
||||
'atlas.bucketWhen': 'Wann möchtest du dorthin reisen?',
|
||||
'atlas.statsTab': 'Statistik',
|
||||
'atlas.bucketTab': 'Bucket List',
|
||||
'atlas.bucketTab': 'Wunschliste',
|
||||
'atlas.addBucket': 'Zur Bucket List hinzufügen',
|
||||
'atlas.bucketNotesPlaceholder': 'Notizen (optional)',
|
||||
'atlas.bucketEmpty': 'Deine Bucket List ist leer',
|
||||
@@ -744,7 +783,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'atlas.lastTrip': 'Letzter Trip',
|
||||
'atlas.nextTrip': 'Nächster Trip',
|
||||
'atlas.daysLeft': 'Tage',
|
||||
'atlas.streak': 'Streak',
|
||||
'atlas.streak': 'Serie',
|
||||
'atlas.years': 'Jahre',
|
||||
'atlas.yearInRow': 'Jahr in Folge',
|
||||
'atlas.yearsInRow': 'Jahre in Folge',
|
||||
@@ -856,7 +895,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'places.noCategory': 'Keine Kategorie',
|
||||
'places.categoryNamePlaceholder': 'Kategoriename',
|
||||
'places.formTime': 'Uhrzeit',
|
||||
'places.startTime': 'Start',
|
||||
'places.startTime': 'Startzeit',
|
||||
'places.endTime': 'Ende',
|
||||
'places.endTimeBeforeStart': 'Endzeit liegt vor der Startzeit',
|
||||
'places.timeCollision': 'Zeitliche Überschneidung mit:',
|
||||
@@ -911,7 +950,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'reservations.timeAlt': 'Uhrzeit (alternativ, z.B. 19:30)',
|
||||
'reservations.notes': 'Notizen',
|
||||
'reservations.notesPlaceholder': 'Zusätzliche Notizen...',
|
||||
'reservations.meta.airline': 'Airline',
|
||||
'reservations.meta.airline': 'Fluggesellschaft',
|
||||
'reservations.meta.flightNumber': 'Flugnr.',
|
||||
'reservations.meta.from': 'Von',
|
||||
'reservations.meta.to': 'Nach',
|
||||
@@ -1407,8 +1446,6 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'memories.reviewTitle': 'Deine Fotos prüfen',
|
||||
'memories.reviewHint': 'Klicke auf Fotos, um sie vom Teilen auszuschließen.',
|
||||
'memories.shareCount': '{count} Fotos teilen',
|
||||
'memories.immichUrl': 'Immich Server URL',
|
||||
'memories.immichApiKey': 'API-Schlüssel',
|
||||
'memories.testConnection': 'Verbindung testen',
|
||||
'memories.testFirst': 'Verbindung zuerst testen',
|
||||
'memories.connected': 'Verbunden',
|
||||
@@ -1609,7 +1646,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
|
||||
// Todo
|
||||
'todo.subtab.packing': 'Packliste',
|
||||
'todo.subtab.todo': 'To-Do',
|
||||
'todo.subtab.todo': 'Aufgaben',
|
||||
'todo.completed': 'erledigt',
|
||||
'todo.filter.all': 'Alle',
|
||||
'todo.filter.open': 'Offen',
|
||||
@@ -1644,7 +1681,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
// Notification system (added from feat/notification-system)
|
||||
'settings.notifyVersionAvailable': 'Neue Version verfügbar',
|
||||
'settings.notificationPreferences.noChannels': 'Keine Benachrichtigungskanäle konfiguriert. Bitte einen Administrator, E-Mail- oder Webhook-Benachrichtigungen einzurichten.',
|
||||
'settings.webhookUrl.label': 'Webhook URL',
|
||||
'settings.webhookUrl.label': 'Webhook-URL',
|
||||
'settings.webhookUrl.placeholder': 'https://discord.com/api/webhooks/...',
|
||||
'settings.webhookUrl.hint': 'Gib deine Discord-, Slack- oder benutzerdefinierte Webhook-URL ein, um Benachrichtigungen zu erhalten.',
|
||||
'settings.webhookUrl.save': 'Speichern',
|
||||
@@ -1705,6 +1742,70 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'notif.generic.text': 'Du hast eine neue Benachrichtigung',
|
||||
'notif.dev.unknown_event.title': '[DEV] Unbekanntes Ereignis',
|
||||
'notif.dev.unknown_event.text': 'Ereignistyp "{event}" ist nicht in EVENT_NOTIFICATION_CONFIG registriert',
|
||||
|
||||
// OAuth scope groups
|
||||
'oauth.scope.group.trips': 'Reisen',
|
||||
'oauth.scope.group.places': 'Orte',
|
||||
'oauth.scope.group.atlas': 'Atlas',
|
||||
'oauth.scope.group.packing': 'Packliste',
|
||||
'oauth.scope.group.todos': 'Aufgaben',
|
||||
'oauth.scope.group.budget': 'Budget',
|
||||
'oauth.scope.group.reservations': 'Buchungen',
|
||||
'oauth.scope.group.collab': 'Zusammenarbeit',
|
||||
'oauth.scope.group.notifications': 'Benachrichtigungen',
|
||||
'oauth.scope.group.vacay': 'Urlaub',
|
||||
'oauth.scope.group.geo': 'Geo',
|
||||
'oauth.scope.group.weather': 'Wetter',
|
||||
|
||||
// OAuth scope labels & descriptions
|
||||
'oauth.scope.trips:read.label': 'Reisen und Reisepläne anzeigen',
|
||||
'oauth.scope.trips:read.description': 'Reisen, Tage, Tagesnotizen und Mitglieder lesen',
|
||||
'oauth.scope.trips:write.label': 'Reisen und Reisepläne bearbeiten',
|
||||
'oauth.scope.trips:write.description': 'Reisen, Tage und Notizen erstellen, aktualisieren und Mitglieder verwalten',
|
||||
'oauth.scope.trips:delete.label': 'Reisen löschen',
|
||||
'oauth.scope.trips:delete.description': 'Reisen dauerhaft löschen — diese Aktion ist unwiderruflich',
|
||||
'oauth.scope.trips:share.label': 'Freigabelinks verwalten',
|
||||
'oauth.scope.trips:share.description': 'Öffentliche Freigabelinks erstellen, aktualisieren und widerrufen',
|
||||
'oauth.scope.places:read.label': 'Orte und Kartendaten anzeigen',
|
||||
'oauth.scope.places:read.description': 'Orte, Tageszuweisungen, Tags und Kategorien lesen',
|
||||
'oauth.scope.places:write.label': 'Orte verwalten',
|
||||
'oauth.scope.places:write.description': 'Orte, Zuweisungen und Tags erstellen, aktualisieren und löschen',
|
||||
'oauth.scope.atlas:read.label': 'Atlas anzeigen',
|
||||
'oauth.scope.atlas:read.description': 'Besuchte Länder, Regionen und Wunschliste lesen',
|
||||
'oauth.scope.atlas:write.label': 'Atlas verwalten',
|
||||
'oauth.scope.atlas:write.description': 'Länder und Regionen als besucht markieren, Wunschliste verwalten',
|
||||
'oauth.scope.packing:read.label': 'Packlisten anzeigen',
|
||||
'oauth.scope.packing:read.description': 'Packgegenstände, Taschen und Kategoriezuweisungen lesen',
|
||||
'oauth.scope.packing:write.label': 'Packlisten verwalten',
|
||||
'oauth.scope.packing:write.description': 'Packgegenstände und Taschen hinzufügen, aktualisieren, löschen, abhaken und sortieren',
|
||||
'oauth.scope.todos:read.label': 'Aufgabenlisten anzeigen',
|
||||
'oauth.scope.todos:read.description': 'Reiseaufgaben und Kategoriezuweisungen lesen',
|
||||
'oauth.scope.todos:write.label': 'Aufgabenlisten verwalten',
|
||||
'oauth.scope.todos:write.description': 'Aufgaben erstellen, aktualisieren, abhaken, löschen und sortieren',
|
||||
'oauth.scope.budget:read.label': 'Budget anzeigen',
|
||||
'oauth.scope.budget:read.description': 'Budgeteinträge und Ausgabenaufschlüsselung lesen',
|
||||
'oauth.scope.budget:write.label': 'Budget verwalten',
|
||||
'oauth.scope.budget:write.description': 'Budgeteinträge erstellen, aktualisieren und löschen',
|
||||
'oauth.scope.reservations:read.label': 'Buchungen anzeigen',
|
||||
'oauth.scope.reservations:read.description': 'Buchungen und Unterkunftsdetails lesen',
|
||||
'oauth.scope.reservations:write.label': 'Buchungen verwalten',
|
||||
'oauth.scope.reservations:write.description': 'Buchungen erstellen, aktualisieren, löschen und sortieren',
|
||||
'oauth.scope.collab:read.label': 'Zusammenarbeit anzeigen',
|
||||
'oauth.scope.collab:read.description': 'Kollaborationsnotizen, Umfragen und Nachrichten lesen',
|
||||
'oauth.scope.collab:write.label': 'Zusammenarbeit verwalten',
|
||||
'oauth.scope.collab:write.description': 'Kollaborationsnotizen, Umfragen und Nachrichten erstellen, aktualisieren und löschen',
|
||||
'oauth.scope.notifications:read.label': 'Benachrichtigungen anzeigen',
|
||||
'oauth.scope.notifications:read.description': 'In-App-Benachrichtigungen und ungelesene Zählungen lesen',
|
||||
'oauth.scope.notifications:write.label': 'Benachrichtigungen verwalten',
|
||||
'oauth.scope.notifications:write.description': 'Benachrichtigungen als gelesen markieren und darauf reagieren',
|
||||
'oauth.scope.vacay:read.label': 'Urlaubspläne anzeigen',
|
||||
'oauth.scope.vacay:read.description': 'Urlaubsplanungsdaten, Einträge und Statistiken lesen',
|
||||
'oauth.scope.vacay:write.label': 'Urlaubspläne verwalten',
|
||||
'oauth.scope.vacay:write.description': 'Urlaubseinträge, Feiertage und Teampläne erstellen und verwalten',
|
||||
'oauth.scope.geo:read.label': 'Karten & Geocodierung',
|
||||
'oauth.scope.geo:read.description': 'Orte suchen, Karten-URLs auflösen und Koordinaten rückwärts geokodieren',
|
||||
'oauth.scope.weather:read.label': 'Wettervorhersagen',
|
||||
'oauth.scope.weather:read.description': 'Wettervorhersagen für Reiseorte und -daten abrufen',
|
||||
}
|
||||
|
||||
export default de
|
||||
|
||||
@@ -1753,6 +1753,70 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
||||
'notif.generic.text': 'You have a new notification',
|
||||
'notif.dev.unknown_event.title': '[DEV] Unknown Event',
|
||||
'notif.dev.unknown_event.text': 'Event type "{event}" is not registered in EVENT_NOTIFICATION_CONFIG',
|
||||
|
||||
// OAuth scope groups
|
||||
'oauth.scope.group.trips': 'Trips',
|
||||
'oauth.scope.group.places': 'Places',
|
||||
'oauth.scope.group.atlas': 'Atlas',
|
||||
'oauth.scope.group.packing': 'Packing',
|
||||
'oauth.scope.group.todos': 'To-dos',
|
||||
'oauth.scope.group.budget': 'Budget',
|
||||
'oauth.scope.group.reservations': 'Reservations',
|
||||
'oauth.scope.group.collab': 'Collaboration',
|
||||
'oauth.scope.group.notifications': 'Notifications',
|
||||
'oauth.scope.group.vacay': 'Vacation',
|
||||
'oauth.scope.group.geo': 'Geo',
|
||||
'oauth.scope.group.weather': 'Weather',
|
||||
|
||||
// OAuth scope labels & descriptions
|
||||
'oauth.scope.trips:read.label': 'View trips & itineraries',
|
||||
'oauth.scope.trips:read.description': 'Read trips, days, day notes, and members',
|
||||
'oauth.scope.trips:write.label': 'Edit trips & itineraries',
|
||||
'oauth.scope.trips:write.description': 'Create and update trips, days, notes, and manage members',
|
||||
'oauth.scope.trips:delete.label': 'Delete trips',
|
||||
'oauth.scope.trips:delete.description': 'Permanently delete entire trips — this action is irreversible',
|
||||
'oauth.scope.trips:share.label': 'Manage share links',
|
||||
'oauth.scope.trips:share.description': 'Create, update, and revoke public share links for trips',
|
||||
'oauth.scope.places:read.label': 'View places & map data',
|
||||
'oauth.scope.places:read.description': 'Read places, day assignments, tags, and categories',
|
||||
'oauth.scope.places:write.label': 'Manage places',
|
||||
'oauth.scope.places:write.description': 'Create, update, and delete places, assignments, and tags',
|
||||
'oauth.scope.atlas:read.label': 'View Atlas',
|
||||
'oauth.scope.atlas:read.description': 'Read visited countries, regions, and bucket list',
|
||||
'oauth.scope.atlas:write.label': 'Manage Atlas',
|
||||
'oauth.scope.atlas:write.description': 'Mark countries and regions visited, manage bucket list',
|
||||
'oauth.scope.packing:read.label': 'View packing lists',
|
||||
'oauth.scope.packing:read.description': 'Read packing items, bags, and category assignees',
|
||||
'oauth.scope.packing:write.label': 'Manage packing lists',
|
||||
'oauth.scope.packing:write.description': 'Add, update, delete, toggle, and reorder packing items and bags',
|
||||
'oauth.scope.todos:read.label': 'View to-do lists',
|
||||
'oauth.scope.todos:read.description': 'Read trip to-do items and category assignees',
|
||||
'oauth.scope.todos:write.label': 'Manage to-do lists',
|
||||
'oauth.scope.todos:write.description': 'Create, update, toggle, delete, and reorder to-do items',
|
||||
'oauth.scope.budget:read.label': 'View budget',
|
||||
'oauth.scope.budget:read.description': 'Read budget items and expense breakdown',
|
||||
'oauth.scope.budget:write.label': 'Manage budget',
|
||||
'oauth.scope.budget:write.description': 'Create, update, and delete budget items',
|
||||
'oauth.scope.reservations:read.label': 'View reservations',
|
||||
'oauth.scope.reservations:read.description': 'Read reservations and accommodation details',
|
||||
'oauth.scope.reservations:write.label': 'Manage reservations',
|
||||
'oauth.scope.reservations:write.description': 'Create, update, delete, and reorder reservations',
|
||||
'oauth.scope.collab:read.label': 'View collaboration',
|
||||
'oauth.scope.collab:read.description': 'Read collab notes, polls, and messages',
|
||||
'oauth.scope.collab:write.label': 'Manage collaboration',
|
||||
'oauth.scope.collab:write.description': 'Create, update, and delete collab notes, polls, and messages',
|
||||
'oauth.scope.notifications:read.label': 'View notifications',
|
||||
'oauth.scope.notifications:read.description': 'Read in-app notifications and unread counts',
|
||||
'oauth.scope.notifications:write.label': 'Manage notifications',
|
||||
'oauth.scope.notifications:write.description': 'Mark notifications as read and respond to them',
|
||||
'oauth.scope.vacay:read.label': 'View vacation plans',
|
||||
'oauth.scope.vacay:read.description': 'Read vacation planning data, entries, and stats',
|
||||
'oauth.scope.vacay:write.label': 'Manage vacation plans',
|
||||
'oauth.scope.vacay:write.description': 'Create and manage vacation entries, holidays, and team plans',
|
||||
'oauth.scope.geo:read.label': 'Maps & geocoding',
|
||||
'oauth.scope.geo:read.description': 'Search locations, resolve map URLs, and reverse geocode coordinates',
|
||||
'oauth.scope.weather:read.label': 'Weather forecasts',
|
||||
'oauth.scope.weather:read.description': 'Fetch weather forecasts for trip locations and dates',
|
||||
}
|
||||
|
||||
export default en
|
||||
|
||||
@@ -180,9 +180,6 @@ const es: Record<string, string> = {
|
||||
'admin.notifications.none': 'Desactivado',
|
||||
'admin.notifications.email': 'Correo (SMTP)',
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'Eventos de notificación',
|
||||
'admin.notifications.eventsHint': 'Elige qué eventos activan notificaciones para todos los usuarios.',
|
||||
'admin.notifications.configureFirst': 'Configura primero los ajustes SMTP o webhook a continuación, luego activa los eventos.',
|
||||
'admin.notifications.save': 'Guardar configuración de notificaciones',
|
||||
'admin.notifications.saved': 'Configuración de notificaciones guardada',
|
||||
'admin.notifications.testWebhook': 'Enviar webhook de prueba',
|
||||
@@ -229,7 +226,7 @@ const es: Record<string, string> = {
|
||||
'settings.mcp.endpoint': 'Endpoint MCP',
|
||||
'settings.mcp.clientConfig': 'Configuración del cliente',
|
||||
'settings.mcp.clientConfigHint': 'Reemplaza <your_token> con un token de la lista de abajo. Es posible que debas ajustar la ruta de npx según tu sistema (p. ej. C:\\PROGRA~1\\nodejs\\npx.cmd en Windows).',
|
||||
'settings.mcp.clientConfigHintOAuth': 'Replace <your_client_id> and <your_client_secret> with the credentials shown in the OAuth 2.1 client you created above. mcp-remote will open your browser to complete the authorization the first time you connect. The path to npx may need to be adjusted for your system (e.g. C:\PROGRA~1\nodejs\npx.cmd on Windows).',
|
||||
'settings.mcp.clientConfigHintOAuth': 'Reemplaza <your_client_id> y <your_client_secret> con las credenciales del cliente OAuth 2.1 que creaste arriba. mcp-remote abrirá el navegador para completar la autorización la primera vez que te conectes. Es posible que debas ajustar la ruta de npx según tu sistema (p. ej. C:\\PROGRA~1\\nodejs\\npx.cmd en Windows).',
|
||||
'settings.mcp.copy': 'Copiar',
|
||||
'settings.mcp.copied': '¡Copiado!',
|
||||
'settings.mcp.apiTokens': 'Tokens de API',
|
||||
@@ -251,6 +248,48 @@ const es: Record<string, string> = {
|
||||
'settings.mcp.toast.createError': 'Error al crear el token',
|
||||
'settings.mcp.toast.deleted': 'Token eliminado',
|
||||
'settings.mcp.toast.deleteError': 'Error al eliminar el token',
|
||||
'settings.mcp.apiTokensDeprecated': 'Los tokens de API están obsoletos y se eliminarán en una versión futura. Utilice los clientes OAuth 2.1 en su lugar.',
|
||||
'settings.oauth.clients': 'Clientes OAuth 2.1',
|
||||
'settings.oauth.clientsHint': 'Registre clientes OAuth 2.1 para que las aplicaciones MCP de terceros (Claude Web, Cursor, etc.) puedan conectarse sin tokens estáticos.',
|
||||
'settings.oauth.createClient': 'Nuevo cliente',
|
||||
'settings.oauth.noClients': 'No hay clientes OAuth registrados.',
|
||||
'settings.oauth.clientId': 'ID de cliente',
|
||||
'settings.oauth.clientSecret': 'Secreto de cliente',
|
||||
'settings.oauth.deleteClient': 'Eliminar cliente',
|
||||
'settings.oauth.deleteClientMessage': 'Este cliente y todas las sesiones activas se eliminarán permanentemente. Cualquier aplicación que lo use perderá el acceso inmediatamente.',
|
||||
'settings.oauth.rotateSecret': 'Renovar secreto',
|
||||
'settings.oauth.rotateSecretMessage': 'Se generará un nuevo secreto de cliente y todas las sesiones existentes se invalidarán de inmediato. Actualice su aplicación antes de cerrar este diálogo.',
|
||||
'settings.oauth.rotateSecretConfirm': 'Renovar',
|
||||
'settings.oauth.rotateSecretConfirming': 'Renovando…',
|
||||
'settings.oauth.rotateSecretDoneTitle': 'Nuevo secreto generado',
|
||||
'settings.oauth.rotateSecretDoneWarning': 'Este secreto solo se muestra una vez. Cópielo ahora y actualice su aplicación — todas las sesiones anteriores han sido invalidadas.',
|
||||
'settings.oauth.activeSessions': 'Sesiones OAuth activas',
|
||||
'settings.oauth.sessionScopes': 'Ámbitos',
|
||||
'settings.oauth.sessionExpires': 'Expira',
|
||||
'settings.oauth.revoke': 'Revocar',
|
||||
'settings.oauth.revokeSession': 'Revocar sesión',
|
||||
'settings.oauth.revokeSessionMessage': 'Esto revocará inmediatamente el acceso de esta sesión OAuth.',
|
||||
'settings.oauth.modal.createTitle': 'Registrar cliente OAuth',
|
||||
'settings.oauth.modal.presets': 'Ajustes rápidos',
|
||||
'settings.oauth.modal.clientName': 'Nombre de la aplicación',
|
||||
'settings.oauth.modal.clientNamePlaceholder': 'ej. Claude Web, Mi app MCP',
|
||||
'settings.oauth.modal.redirectUris': 'URIs de redirección',
|
||||
'settings.oauth.modal.redirectUrisPlaceholder': 'https://your-app.com/callback\nhttps://your-app.com/auth',
|
||||
'settings.oauth.modal.redirectUrisHint': 'Un URI por línea. HTTPS obligatorio (localhost exento). Coincidencia exacta.',
|
||||
'settings.oauth.modal.scopes': 'Ámbitos permitidos',
|
||||
'settings.oauth.modal.scopesHint': 'list_trips y get_trip_summary siempre están disponibles — sin ámbito requerido. Permiten a la IA descubrir los IDs de viaje necesarios.',
|
||||
'settings.oauth.modal.selectAll': 'Seleccionar todo',
|
||||
'settings.oauth.modal.deselectAll': 'Deseleccionar todo',
|
||||
'settings.oauth.modal.creating': 'Registrando…',
|
||||
'settings.oauth.modal.create': 'Registrar cliente',
|
||||
'settings.oauth.modal.createdTitle': 'Cliente registrado',
|
||||
'settings.oauth.modal.createdWarning': 'El secreto del cliente solo se muestra una vez. Cópielo ahora — no se puede recuperar.',
|
||||
'settings.oauth.toast.createError': 'Error al registrar el cliente OAuth',
|
||||
'settings.oauth.toast.deleted': 'Cliente OAuth eliminado',
|
||||
'settings.oauth.toast.deleteError': 'Error al eliminar el cliente OAuth',
|
||||
'settings.oauth.toast.revoked': 'Sesión revocada',
|
||||
'settings.oauth.toast.revokeError': 'Error al revocar la sesión',
|
||||
'settings.oauth.toast.rotateError': 'Error al renovar el secreto del cliente',
|
||||
'settings.account': 'Cuenta',
|
||||
'settings.about': 'Acerca de',
|
||||
'settings.about.reportBug': 'Reportar un error',
|
||||
@@ -398,7 +437,7 @@ const es: Record<string, string> = {
|
||||
'admin.tabs.users': 'Usuarios',
|
||||
'admin.tabs.categories': 'Categorías',
|
||||
'admin.tabs.backup': 'Copia de seguridad',
|
||||
'admin.tabs.audit': 'Audit',
|
||||
'admin.tabs.audit': 'Auditoría',
|
||||
'admin.stats.users': 'Usuarios',
|
||||
'admin.stats.trips': 'Viajes',
|
||||
'admin.stats.places': 'Lugares',
|
||||
@@ -681,7 +720,7 @@ const es: Record<string, string> = {
|
||||
'vacay.fuseInfo4': 'Ajustes como festivos y festivos de empresa se comparten.',
|
||||
'vacay.fuseInfo5': 'La fusión puede disolverse en cualquier momento por cualquiera de las partes. Tus entradas se conservarán.',
|
||||
'vacay.addCalendar': 'Añadir calendario',
|
||||
'vacay.calendarColor': 'Color',
|
||||
'vacay.calendarColor': 'Color del calendario',
|
||||
'vacay.calendarLabel': 'Etiqueta',
|
||||
'vacay.noCalendars': 'Sin calendarios',
|
||||
|
||||
@@ -894,7 +933,7 @@ const es: Record<string, string> = {
|
||||
'reservations.type.car': 'Coche de alquiler',
|
||||
'reservations.type.cruise': 'Crucero',
|
||||
'reservations.type.event': 'Evento',
|
||||
'reservations.type.tour': 'Tour',
|
||||
'reservations.type.tour': 'Excursión',
|
||||
'reservations.type.other': 'Otro',
|
||||
'reservations.confirm.delete': '¿Seguro que quieres eliminar la reserva "{name}"?',
|
||||
'reservations.confirm.deleteTitle': '¿Eliminar reserva?',
|
||||
@@ -978,6 +1017,7 @@ const es: Record<string, string> = {
|
||||
'budget.totalBudget': 'Presupuesto total',
|
||||
'budget.byCategory': 'Por categoría',
|
||||
'budget.editTooltip': 'Haz clic para editar',
|
||||
'budget.linkedToReservation': 'Vinculado a una reserva — edite el nombre allí',
|
||||
'budget.confirm.deleteCategory': '¿Seguro que quieres eliminar la categoría "{name}" con {count} entradas?',
|
||||
'budget.deleteCategory': 'Eliminar categoría',
|
||||
'budget.perPerson': 'Por persona',
|
||||
@@ -1054,6 +1094,9 @@ const es: Record<string, string> = {
|
||||
'packing.template': 'Plantilla',
|
||||
'packing.templateApplied': '{count} artículos añadidos desde plantilla',
|
||||
'packing.templateError': 'Error al aplicar plantilla',
|
||||
'packing.saveAsTemplate': 'Guardar como plantilla',
|
||||
'packing.templateName': 'Nombre de la plantilla',
|
||||
'packing.templateSaved': 'Lista de equipaje guardada como plantilla',
|
||||
'packing.assignUser': 'Asignar usuario',
|
||||
'packing.noMembers': 'Sin miembros',
|
||||
'packing.bags': 'Equipaje',
|
||||
@@ -1334,8 +1377,8 @@ const es: Record<string, string> = {
|
||||
'day.hotelDayRange': 'Aplicar a los días',
|
||||
'day.noPlacesForHotel': 'Añade primero lugares al viaje',
|
||||
'day.allDays': 'Todos',
|
||||
'day.checkIn': 'Check-in',
|
||||
'day.checkOut': 'Check-out',
|
||||
'day.checkIn': 'Registro de entrada',
|
||||
'day.checkOut': 'Registro de salida',
|
||||
'day.confirmation': 'Confirmación',
|
||||
'day.editAccommodation': 'Editar alojamiento',
|
||||
'day.reservations': 'Reservas',
|
||||
@@ -1354,8 +1397,6 @@ const es: Record<string, string> = {
|
||||
'memories.reviewTitle': 'Revisar tus fotos',
|
||||
'memories.reviewHint': 'Haz clic en las fotos para excluirlas de compartir.',
|
||||
'memories.shareCount': 'Compartir {count} fotos',
|
||||
'memories.immichUrl': 'URL del servidor Immich',
|
||||
'memories.immichApiKey': 'Clave API',
|
||||
'memories.testConnection': 'Probar conexión',
|
||||
'memories.testFirst': 'Probar conexión primero',
|
||||
'memories.connected': 'Conectado',
|
||||
@@ -1488,8 +1529,8 @@ const es: Record<string, string> = {
|
||||
'reservations.meta.trainNumber': 'N° de tren',
|
||||
'reservations.meta.platform': 'Andén',
|
||||
'reservations.meta.seat': 'Asiento',
|
||||
'reservations.meta.checkIn': 'Check-in',
|
||||
'reservations.meta.checkOut': 'Check-out',
|
||||
'reservations.meta.checkIn': 'Registro de entrada',
|
||||
'reservations.meta.checkOut': 'Registro de salida',
|
||||
'reservations.meta.linkAccommodation': 'Alojamiento',
|
||||
'reservations.meta.pickAccommodation': 'Vincular con alojamiento',
|
||||
'reservations.meta.noAccommodation': 'Ninguno',
|
||||
@@ -1705,6 +1746,70 @@ const es: Record<string, string> = {
|
||||
'notif.generic.text': 'Tienes una nueva notificación',
|
||||
'notif.dev.unknown_event.title': '[DEV] Evento desconocido',
|
||||
'notif.dev.unknown_event.text': 'El tipo de evento "{event}" no está registrado en EVENT_NOTIFICATION_CONFIG',
|
||||
|
||||
// OAuth scope groups
|
||||
'oauth.scope.group.trips': 'Viajes',
|
||||
'oauth.scope.group.places': 'Lugares',
|
||||
'oauth.scope.group.atlas': 'Atlas',
|
||||
'oauth.scope.group.packing': 'Equipaje',
|
||||
'oauth.scope.group.todos': 'Tareas',
|
||||
'oauth.scope.group.budget': 'Presupuesto',
|
||||
'oauth.scope.group.reservations': 'Reservas',
|
||||
'oauth.scope.group.collab': 'Colaboración',
|
||||
'oauth.scope.group.notifications': 'Notificaciones',
|
||||
'oauth.scope.group.vacay': 'Vacaciones',
|
||||
'oauth.scope.group.geo': 'Geo',
|
||||
'oauth.scope.group.weather': 'Clima',
|
||||
|
||||
// OAuth scope labels & descriptions
|
||||
'oauth.scope.trips:read.label': 'Ver viajes e itinerarios',
|
||||
'oauth.scope.trips:read.description': 'Leer viajes, días, notas y miembros',
|
||||
'oauth.scope.trips:write.label': 'Editar viajes e itinerarios',
|
||||
'oauth.scope.trips:write.description': 'Crear y actualizar viajes, días, notas y gestionar miembros',
|
||||
'oauth.scope.trips:delete.label': 'Eliminar viajes',
|
||||
'oauth.scope.trips:delete.description': 'Eliminar viajes permanentemente — esta acción es irreversible',
|
||||
'oauth.scope.trips:share.label': 'Gestionar enlaces de compartir',
|
||||
'oauth.scope.trips:share.description': 'Crear, actualizar y revocar enlaces públicos de viaje',
|
||||
'oauth.scope.places:read.label': 'Ver lugares y datos del mapa',
|
||||
'oauth.scope.places:read.description': 'Leer lugares, asignaciones de días, etiquetas y categorías',
|
||||
'oauth.scope.places:write.label': 'Gestionar lugares',
|
||||
'oauth.scope.places:write.description': 'Crear, actualizar y eliminar lugares, asignaciones y etiquetas',
|
||||
'oauth.scope.atlas:read.label': 'Ver Atlas',
|
||||
'oauth.scope.atlas:read.description': 'Leer países visitados, regiones y lista de deseos',
|
||||
'oauth.scope.atlas:write.label': 'Gestionar Atlas',
|
||||
'oauth.scope.atlas:write.description': 'Marcar países y regiones como visitados, gestionar lista de deseos',
|
||||
'oauth.scope.packing:read.label': 'Ver listas de equipaje',
|
||||
'oauth.scope.packing:read.description': 'Leer artículos, maletas y responsables de categoría',
|
||||
'oauth.scope.packing:write.label': 'Gestionar listas de equipaje',
|
||||
'oauth.scope.packing:write.description': 'Agregar, actualizar, eliminar, marcar y reordenar artículos y maletas',
|
||||
'oauth.scope.todos:read.label': 'Ver listas de tareas',
|
||||
'oauth.scope.todos:read.description': 'Leer tareas del viaje y responsables de categoría',
|
||||
'oauth.scope.todos:write.label': 'Gestionar listas de tareas',
|
||||
'oauth.scope.todos:write.description': 'Crear, actualizar, marcar, eliminar y reordenar tareas',
|
||||
'oauth.scope.budget:read.label': 'Ver presupuesto',
|
||||
'oauth.scope.budget:read.description': 'Leer partidas de presupuesto y desglose de gastos',
|
||||
'oauth.scope.budget:write.label': 'Gestionar presupuesto',
|
||||
'oauth.scope.budget:write.description': 'Crear, actualizar y eliminar partidas de presupuesto',
|
||||
'oauth.scope.reservations:read.label': 'Ver reservas',
|
||||
'oauth.scope.reservations:read.description': 'Leer reservas y detalles de alojamiento',
|
||||
'oauth.scope.reservations:write.label': 'Gestionar reservas',
|
||||
'oauth.scope.reservations:write.description': 'Crear, actualizar, eliminar y reordenar reservas',
|
||||
'oauth.scope.collab:read.label': 'Ver colaboración',
|
||||
'oauth.scope.collab:read.description': 'Leer notas colaborativas, encuestas y mensajes',
|
||||
'oauth.scope.collab:write.label': 'Gestionar colaboración',
|
||||
'oauth.scope.collab:write.description': 'Crear, actualizar y eliminar notas, encuestas y mensajes',
|
||||
'oauth.scope.notifications:read.label': 'Ver notificaciones',
|
||||
'oauth.scope.notifications:read.description': 'Leer notificaciones y conteos no leídos',
|
||||
'oauth.scope.notifications:write.label': 'Gestionar notificaciones',
|
||||
'oauth.scope.notifications:write.description': 'Marcar notificaciones como leídas y responderlas',
|
||||
'oauth.scope.vacay:read.label': 'Ver planes de vacaciones',
|
||||
'oauth.scope.vacay:read.description': 'Leer datos de planificación, entradas y estadísticas de vacaciones',
|
||||
'oauth.scope.vacay:write.label': 'Gestionar planes de vacaciones',
|
||||
'oauth.scope.vacay:write.description': 'Crear y gestionar entradas de vacaciones, festivos y planes de equipo',
|
||||
'oauth.scope.geo:read.label': 'Mapas y geocodificación',
|
||||
'oauth.scope.geo:read.description': 'Buscar lugares, resolver URLs de mapa y geocodificar coordenadas',
|
||||
'oauth.scope.weather:read.label': 'Previsiones meteorológicas',
|
||||
'oauth.scope.weather:read.description': 'Obtener previsiones meteorológicas para lugares y fechas del viaje',
|
||||
}
|
||||
|
||||
export default es
|
||||
|
||||
@@ -179,9 +179,6 @@ const fr: Record<string, string> = {
|
||||
'admin.notifications.none': 'Désactivé',
|
||||
'admin.notifications.email': 'E-mail (SMTP)',
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'Événements de notification',
|
||||
'admin.notifications.eventsHint': 'Choisissez quels événements déclenchent des notifications pour tous les utilisateurs.',
|
||||
'admin.notifications.configureFirst': 'Configurez d\'abord les paramètres SMTP ou webhook ci-dessous, puis activez les événements.',
|
||||
'admin.notifications.save': 'Enregistrer les paramètres de notification',
|
||||
'admin.notifications.saved': 'Paramètres de notification enregistrés',
|
||||
'admin.notifications.testWebhook': 'Envoyer un webhook de test',
|
||||
@@ -228,7 +225,7 @@ const fr: Record<string, string> = {
|
||||
'settings.mcp.endpoint': 'Point de terminaison MCP',
|
||||
'settings.mcp.clientConfig': 'Configuration du client',
|
||||
'settings.mcp.clientConfigHint': 'Remplacez <your_token> par un token API de la liste ci-dessous. Le chemin vers npx devra peut-être être ajusté selon votre système (ex. C:\\PROGRA~1\\nodejs\\npx.cmd sous Windows).',
|
||||
'settings.mcp.clientConfigHintOAuth': 'Replace <your_client_id> and <your_client_secret> with the credentials shown in the OAuth 2.1 client you created above. mcp-remote will open your browser to complete the authorization the first time you connect. The path to npx may need to be adjusted for your system (e.g. C:\PROGRA~1\nodejs\npx.cmd on Windows).',
|
||||
'settings.mcp.clientConfigHintOAuth': 'Remplacez <your_client_id> et <your_client_secret> par les identifiants affichés dans le client OAuth 2.1 créé ci-dessus. mcp-remote ouvrira votre navigateur pour finaliser l\'autorisation lors de la première connexion. Le chemin vers npx devra peut-être être ajusté selon votre système (ex. C:\\PROGRA~1\\nodejs\\npx.cmd sous Windows).',
|
||||
'settings.mcp.copy': 'Copier',
|
||||
'settings.mcp.copied': 'Copié !',
|
||||
'settings.mcp.apiTokens': 'Tokens API',
|
||||
@@ -250,6 +247,48 @@ const fr: Record<string, string> = {
|
||||
'settings.mcp.toast.createError': 'Impossible de créer le token',
|
||||
'settings.mcp.toast.deleted': 'Token supprimé',
|
||||
'settings.mcp.toast.deleteError': 'Impossible de supprimer le token',
|
||||
'settings.mcp.apiTokensDeprecated': 'Les tokens API sont dépréciés et seront supprimés dans une prochaine version. Veuillez utiliser les clients OAuth 2.1 à la place.',
|
||||
'settings.oauth.clients': 'Clients OAuth 2.1',
|
||||
'settings.oauth.clientsHint': 'Enregistrez des clients OAuth 2.1 pour permettre à des applications MCP tierces (Claude Web, Cursor, etc.) de se connecter sans tokens statiques.',
|
||||
'settings.oauth.createClient': 'Nouveau client',
|
||||
'settings.oauth.noClients': 'Aucun client OAuth enregistré.',
|
||||
'settings.oauth.clientId': 'ID client',
|
||||
'settings.oauth.clientSecret': 'Secret client',
|
||||
'settings.oauth.deleteClient': 'Supprimer le client',
|
||||
'settings.oauth.deleteClientMessage': 'Ce client et toutes les sessions actives seront définitivement supprimés. Toute application l\'utilisant perdra immédiatement l\'accès.',
|
||||
'settings.oauth.rotateSecret': 'Renouveler le secret',
|
||||
'settings.oauth.rotateSecretMessage': 'Un nouveau secret client sera généré et toutes les sessions existantes seront immédiatement invalidées. Mettez à jour votre application avant de fermer cette fenêtre.',
|
||||
'settings.oauth.rotateSecretConfirm': 'Renouveler',
|
||||
'settings.oauth.rotateSecretConfirming': 'Renouvellement…',
|
||||
'settings.oauth.rotateSecretDoneTitle': 'Nouveau secret généré',
|
||||
'settings.oauth.rotateSecretDoneWarning': 'Ce secret n\'est affiché qu\'une seule fois. Copiez-le maintenant et mettez à jour votre application — toutes les sessions précédentes ont été invalidées.',
|
||||
'settings.oauth.activeSessions': 'Sessions OAuth actives',
|
||||
'settings.oauth.sessionScopes': 'Portées',
|
||||
'settings.oauth.sessionExpires': 'Expire',
|
||||
'settings.oauth.revoke': 'Révoquer',
|
||||
'settings.oauth.revokeSession': 'Révoquer la session',
|
||||
'settings.oauth.revokeSessionMessage': 'Cela révoquera immédiatement l\'accès pour cette session OAuth.',
|
||||
'settings.oauth.modal.createTitle': 'Enregistrer un client OAuth',
|
||||
'settings.oauth.modal.presets': 'Préréglages rapides',
|
||||
'settings.oauth.modal.clientName': 'Nom de l\'application',
|
||||
'settings.oauth.modal.clientNamePlaceholder': 'ex. Claude Web, Mon app MCP',
|
||||
'settings.oauth.modal.redirectUris': 'URIs de redirection',
|
||||
'settings.oauth.modal.redirectUrisPlaceholder': 'https://your-app.com/callback\nhttps://your-app.com/auth',
|
||||
'settings.oauth.modal.redirectUrisHint': 'Une URI par ligne. HTTPS requis (localhost exempté). Correspondance exacte.',
|
||||
'settings.oauth.modal.scopes': 'Portées autorisées',
|
||||
'settings.oauth.modal.scopesHint': 'list_trips et get_trip_summary sont toujours disponibles — aucune portée requise. Ils permettent à l\'IA de découvrir les IDs de voyage nécessaires.',
|
||||
'settings.oauth.modal.selectAll': 'Tout sélectionner',
|
||||
'settings.oauth.modal.deselectAll': 'Tout désélectionner',
|
||||
'settings.oauth.modal.creating': 'Enregistrement…',
|
||||
'settings.oauth.modal.create': 'Enregistrer le client',
|
||||
'settings.oauth.modal.createdTitle': 'Client enregistré',
|
||||
'settings.oauth.modal.createdWarning': 'Le secret client n\'est affiché qu\'une seule fois. Copiez-le maintenant — il ne peut pas être récupéré.',
|
||||
'settings.oauth.toast.createError': 'Impossible d\'enregistrer le client OAuth',
|
||||
'settings.oauth.toast.deleted': 'Client OAuth supprimé',
|
||||
'settings.oauth.toast.deleteError': 'Impossible de supprimer le client OAuth',
|
||||
'settings.oauth.toast.revoked': 'Session révoquée',
|
||||
'settings.oauth.toast.revokeError': 'Impossible de révoquer la session',
|
||||
'settings.oauth.toast.rotateError': 'Impossible de renouveler le secret client',
|
||||
'settings.account': 'Compte',
|
||||
'settings.about': 'À propos',
|
||||
'settings.about.reportBug': 'Signaler un bug',
|
||||
@@ -1018,6 +1057,7 @@ const fr: Record<string, string> = {
|
||||
'budget.totalBudget': 'Budget total',
|
||||
'budget.byCategory': 'Par catégorie',
|
||||
'budget.editTooltip': 'Cliquez pour modifier',
|
||||
'budget.linkedToReservation': 'Lié à une réservation — modifiez le nom depuis celle-ci',
|
||||
'budget.confirm.deleteCategory': 'Voulez-vous vraiment supprimer la catégorie « {name} » avec {count} entrées ?',
|
||||
'budget.deleteCategory': 'Supprimer la catégorie',
|
||||
'budget.perPerson': 'Par personne',
|
||||
@@ -1116,6 +1156,9 @@ const fr: Record<string, string> = {
|
||||
'packing.template': 'Modèle',
|
||||
'packing.templateApplied': '{count} articles ajoutés depuis le modèle',
|
||||
'packing.templateError': 'Erreur lors de l\'application du modèle',
|
||||
'packing.saveAsTemplate': 'Enregistrer comme modèle',
|
||||
'packing.templateName': 'Nom du modèle',
|
||||
'packing.templateSaved': 'Liste de voyage enregistrée comme modèle',
|
||||
'packing.assignUser': 'Assigner un utilisateur',
|
||||
'packing.noMembers': 'Aucun membre',
|
||||
'packing.bags': 'Bagages',
|
||||
@@ -1401,8 +1444,6 @@ const fr: Record<string, string> = {
|
||||
'memories.reviewTitle': 'Vérifier vos photos',
|
||||
'memories.reviewHint': 'Cliquez sur les photos pour les exclure du partage.',
|
||||
'memories.shareCount': 'Partager {count} photos',
|
||||
'memories.immichUrl': 'URL du serveur Immich',
|
||||
'memories.immichApiKey': 'Clé API',
|
||||
'memories.testConnection': 'Tester la connexion',
|
||||
'memories.testFirst': 'Testez la connexion avant de sauvegarder',
|
||||
'memories.connected': 'Connecté',
|
||||
@@ -1699,6 +1740,70 @@ const fr: Record<string, string> = {
|
||||
'notif.generic.text': 'Vous avez une nouvelle notification',
|
||||
'notif.dev.unknown_event.title': '[DEV] Événement inconnu',
|
||||
'notif.dev.unknown_event.text': 'Le type d\'événement "{event}" n\'est pas enregistré dans EVENT_NOTIFICATION_CONFIG',
|
||||
|
||||
// OAuth scope groups
|
||||
'oauth.scope.group.trips': 'Voyages',
|
||||
'oauth.scope.group.places': 'Lieux',
|
||||
'oauth.scope.group.atlas': 'Atlas',
|
||||
'oauth.scope.group.packing': 'Bagages',
|
||||
'oauth.scope.group.todos': 'Tâches',
|
||||
'oauth.scope.group.budget': 'Budget',
|
||||
'oauth.scope.group.reservations': 'Réservations',
|
||||
'oauth.scope.group.collab': 'Collaboration',
|
||||
'oauth.scope.group.notifications': 'Notifications',
|
||||
'oauth.scope.group.vacay': 'Congés',
|
||||
'oauth.scope.group.geo': 'Géo',
|
||||
'oauth.scope.group.weather': 'Météo',
|
||||
|
||||
// OAuth scope labels & descriptions
|
||||
'oauth.scope.trips:read.label': 'Voir les voyages et itinéraires',
|
||||
'oauth.scope.trips:read.description': 'Lire les voyages, jours, notes et membres',
|
||||
'oauth.scope.trips:write.label': 'Modifier les voyages et itinéraires',
|
||||
'oauth.scope.trips:write.description': 'Créer et mettre à jour les voyages, jours, notes et gérer les membres',
|
||||
'oauth.scope.trips:delete.label': 'Supprimer des voyages',
|
||||
'oauth.scope.trips:delete.description': 'Supprimer définitivement des voyages entiers — cette action est irréversible',
|
||||
'oauth.scope.trips:share.label': 'Gérer les liens de partage',
|
||||
'oauth.scope.trips:share.description': 'Créer, modifier et révoquer des liens de partage publics',
|
||||
'oauth.scope.places:read.label': 'Voir les lieux et données cartographiques',
|
||||
'oauth.scope.places:read.description': 'Lire les lieux, affectations de jours, étiquettes et catégories',
|
||||
'oauth.scope.places:write.label': 'Gérer les lieux',
|
||||
'oauth.scope.places:write.description': 'Créer, modifier et supprimer des lieux, affectations et étiquettes',
|
||||
'oauth.scope.atlas:read.label': 'Voir l\'Atlas',
|
||||
'oauth.scope.atlas:read.description': 'Lire les pays visités, régions et liste de souhaits',
|
||||
'oauth.scope.atlas:write.label': 'Gérer l\'Atlas',
|
||||
'oauth.scope.atlas:write.description': 'Marquer des pays et régions visités, gérer la liste de souhaits',
|
||||
'oauth.scope.packing:read.label': 'Voir les listes de bagages',
|
||||
'oauth.scope.packing:read.description': 'Lire les articles, sacs et assignations de catégories',
|
||||
'oauth.scope.packing:write.label': 'Gérer les listes de bagages',
|
||||
'oauth.scope.packing:write.description': 'Ajouter, modifier, supprimer, cocher et réordonner les articles et sacs',
|
||||
'oauth.scope.todos:read.label': 'Voir les listes de tâches',
|
||||
'oauth.scope.todos:read.description': 'Lire les tâches et assignations de catégories',
|
||||
'oauth.scope.todos:write.label': 'Gérer les listes de tâches',
|
||||
'oauth.scope.todos:write.description': 'Créer, modifier, cocher, supprimer et réordonner les tâches',
|
||||
'oauth.scope.budget:read.label': 'Voir le budget',
|
||||
'oauth.scope.budget:read.description': 'Lire les dépenses et la répartition du budget',
|
||||
'oauth.scope.budget:write.label': 'Gérer le budget',
|
||||
'oauth.scope.budget:write.description': 'Créer, modifier et supprimer des dépenses',
|
||||
'oauth.scope.reservations:read.label': 'Voir les réservations',
|
||||
'oauth.scope.reservations:read.description': 'Lire les réservations et détails d\'hébergement',
|
||||
'oauth.scope.reservations:write.label': 'Gérer les réservations',
|
||||
'oauth.scope.reservations:write.description': 'Créer, modifier, supprimer et réordonner les réservations',
|
||||
'oauth.scope.collab:read.label': 'Voir la collaboration',
|
||||
'oauth.scope.collab:read.description': 'Lire les notes, sondages et messages collaboratifs',
|
||||
'oauth.scope.collab:write.label': 'Gérer la collaboration',
|
||||
'oauth.scope.collab:write.description': 'Créer, modifier et supprimer des notes, sondages et messages',
|
||||
'oauth.scope.notifications:read.label': 'Voir les notifications',
|
||||
'oauth.scope.notifications:read.description': 'Lire les notifications et le nombre de non-lus',
|
||||
'oauth.scope.notifications:write.label': 'Gérer les notifications',
|
||||
'oauth.scope.notifications:write.description': 'Marquer les notifications comme lues et y répondre',
|
||||
'oauth.scope.vacay:read.label': 'Voir les plans de congés',
|
||||
'oauth.scope.vacay:read.description': 'Lire les données, entrées et statistiques de congés',
|
||||
'oauth.scope.vacay:write.label': 'Gérer les plans de congés',
|
||||
'oauth.scope.vacay:write.description': 'Créer et gérer les entrées de congés, jours fériés et plans d\'équipe',
|
||||
'oauth.scope.geo:read.label': 'Cartes et géocodage',
|
||||
'oauth.scope.geo:read.description': 'Chercher des lieux, résoudre des URL cartographiques et géocoder des coordonnées',
|
||||
'oauth.scope.weather:read.label': 'Prévisions météo',
|
||||
'oauth.scope.weather:read.description': 'Obtenir les prévisions météo pour les lieux et dates de voyage',
|
||||
}
|
||||
|
||||
export default fr
|
||||
|
||||
@@ -180,7 +180,7 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'settings.mcp.endpoint': 'MCP végpont',
|
||||
'settings.mcp.clientConfig': 'Kliens konfiguráció',
|
||||
'settings.mcp.clientConfigHint': 'Cserélje ki a <your_token> részt egy API tokenre az alábbi listából. Az npx elérési útját szükség lehet módosítani a rendszeréhez (pl. C:\\PROGRA~1\\nodejs\\npx.cmd Windows-on).',
|
||||
'settings.mcp.clientConfigHintOAuth': 'Replace <your_client_id> and <your_client_secret> with the credentials shown in the OAuth 2.1 client you created above. mcp-remote will open your browser to complete the authorization the first time you connect. The path to npx may need to be adjusted for your system (e.g. C:\PROGRA~1\nodejs\npx.cmd on Windows).',
|
||||
'settings.mcp.clientConfigHintOAuth': 'Cserélje ki a <your_client_id> és <your_client_secret> részeket a fent létrehozott OAuth 2.1 kliens adataival. Az mcp-remote megnyitja a böngészőt az első csatlakozáskor az engedélyezés elvégzéséhez. Az npx elérési útját szükség lehet módosítani a rendszeréhez (pl. C:\\PROGRA~1\\nodejs\\npx.cmd Windows-on).',
|
||||
'settings.mcp.copy': 'Másolás',
|
||||
'settings.mcp.copied': 'Másolva!',
|
||||
'settings.mcp.apiTokens': 'API tokenek',
|
||||
@@ -202,6 +202,48 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'settings.mcp.toast.createError': 'Nem sikerült létrehozni a tokent',
|
||||
'settings.mcp.toast.deleted': 'Token törölve',
|
||||
'settings.mcp.toast.deleteError': 'Nem sikerült törölni a tokent',
|
||||
'settings.mcp.apiTokensDeprecated': 'Az API tokenek elavultak és egy jövőbeli verzióban eltávolításra kerülnek. Kérjük, használjon helyettük OAuth 2.1 klienseket.',
|
||||
'settings.oauth.clients': 'OAuth 2.1 kliensek',
|
||||
'settings.oauth.clientsHint': 'Regisztráljon OAuth 2.1 klienseket, hogy a harmadik féltől származó MCP alkalmazások (Claude Web, Cursor stb.) statikus tokenek nélkül csatlakozhassanak.',
|
||||
'settings.oauth.createClient': 'Új kliens',
|
||||
'settings.oauth.noClients': 'Nincs regisztrált OAuth kliens.',
|
||||
'settings.oauth.clientId': 'Kliens azonosító',
|
||||
'settings.oauth.clientSecret': 'Kliens titok',
|
||||
'settings.oauth.deleteClient': 'Kliens törlése',
|
||||
'settings.oauth.deleteClientMessage': 'Ez a kliens és az összes aktív munkamenet véglegesen törlésre kerül. Minden alkalmazás, amely ezt használja, azonnal elveszíti a hozzáférést.',
|
||||
'settings.oauth.rotateSecret': 'Titok megújítása',
|
||||
'settings.oauth.rotateSecretMessage': 'Új kliens titok kerül generálásra és az összes meglévő munkamenet azonnal érvénytelenné válik. Frissítse alkalmazását a párbeszéd bezárása előtt.',
|
||||
'settings.oauth.rotateSecretConfirm': 'Megújítás',
|
||||
'settings.oauth.rotateSecretConfirming': 'Megújítás…',
|
||||
'settings.oauth.rotateSecretDoneTitle': 'Új titok generálva',
|
||||
'settings.oauth.rotateSecretDoneWarning': 'Ez a titok csak egyszer jelenik meg. Másolja most és frissítse alkalmazását — az összes korábbi munkamenet érvénytelenné vált.',
|
||||
'settings.oauth.activeSessions': 'Aktív OAuth munkamenetek',
|
||||
'settings.oauth.sessionScopes': 'Jogosultságok',
|
||||
'settings.oauth.sessionExpires': 'Lejár',
|
||||
'settings.oauth.revoke': 'Visszavonás',
|
||||
'settings.oauth.revokeSession': 'Munkamenet visszavonása',
|
||||
'settings.oauth.revokeSessionMessage': 'Ez azonnal visszavonja a hozzáférést ehhez az OAuth munkamenethez.',
|
||||
'settings.oauth.modal.createTitle': 'OAuth kliens regisztrálása',
|
||||
'settings.oauth.modal.presets': 'Gyors beállítások',
|
||||
'settings.oauth.modal.clientName': 'Alkalmazás neve',
|
||||
'settings.oauth.modal.clientNamePlaceholder': 'pl. Claude Web, Az én MCP appom',
|
||||
'settings.oauth.modal.redirectUris': 'Átirányítási URI-k',
|
||||
'settings.oauth.modal.redirectUrisPlaceholder': 'https://your-app.com/callback\nhttps://your-app.com/auth',
|
||||
'settings.oauth.modal.redirectUrisHint': 'Soronként egy URI. HTTPS szükséges (localhost kivételével). Pontos egyezés szükséges.',
|
||||
'settings.oauth.modal.scopes': 'Engedélyezett jogosultságok',
|
||||
'settings.oauth.modal.scopesHint': 'A list_trips és get_trip_summary mindig elérhető — jogosultság nélkül. Segítenek az AI-nak megtalálni az utazás azonosítókat.',
|
||||
'settings.oauth.modal.selectAll': 'Összes kijelölése',
|
||||
'settings.oauth.modal.deselectAll': 'Összes kijelölés törlése',
|
||||
'settings.oauth.modal.creating': 'Regisztrálás…',
|
||||
'settings.oauth.modal.create': 'Kliens regisztrálása',
|
||||
'settings.oauth.modal.createdTitle': 'Kliens regisztrálva',
|
||||
'settings.oauth.modal.createdWarning': 'A kliens titok csak egyszer jelenik meg. Másolja most — nem állítható helyre.',
|
||||
'settings.oauth.toast.createError': 'Az OAuth kliens regisztrálása sikertelen',
|
||||
'settings.oauth.toast.deleted': 'OAuth kliens törölve',
|
||||
'settings.oauth.toast.deleteError': 'Az OAuth kliens törlése sikertelen',
|
||||
'settings.oauth.toast.revoked': 'Munkamenet visszavonva',
|
||||
'settings.oauth.toast.revokeError': 'A munkamenet visszavonása sikertelen',
|
||||
'settings.oauth.toast.rotateError': 'A kliens titok megújítása sikertelen',
|
||||
'settings.account': 'Fiók',
|
||||
'settings.about': 'Névjegy',
|
||||
'settings.about.reportBug': 'Hiba bejelentése',
|
||||
@@ -275,9 +317,6 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.notifications.none': 'Kikapcsolva',
|
||||
'admin.notifications.email': 'E-mail (SMTP)',
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'Értesítési események',
|
||||
'admin.notifications.eventsHint': 'Válaszd ki, mely események indítsanak értesítéseket minden felhasználó számára.',
|
||||
'admin.notifications.configureFirst': 'Először konfiguráld az SMTP vagy webhook beállításokat lent, majd engedélyezd az eseményeket.',
|
||||
'admin.notifications.save': 'Értesítési beállítások mentése',
|
||||
'admin.notifications.saved': 'Értesítési beállítások mentve',
|
||||
'admin.notifications.testWebhook': 'Teszt webhook küldése',
|
||||
@@ -1019,6 +1058,7 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'budget.totalBudget': 'Teljes költségvetés',
|
||||
'budget.byCategory': 'Kategóriánként',
|
||||
'budget.editTooltip': 'Kattints a szerkesztéshez',
|
||||
'budget.linkedToReservation': 'Foglaláshoz kapcsolva — ott szerkessze a nevet',
|
||||
'budget.confirm.deleteCategory': 'Biztosan törölni szeretnéd a(z) "{name}" kategóriát {count} bejegyzéssel?',
|
||||
'budget.deleteCategory': 'Kategória törlése',
|
||||
'budget.perPerson': 'Személyenként',
|
||||
@@ -1119,6 +1159,9 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'packing.template': 'Sablon',
|
||||
'packing.templateApplied': '{count} tétel hozzáadva a sablonból',
|
||||
'packing.templateError': 'Nem sikerült alkalmazni a sablont',
|
||||
'packing.saveAsTemplate': 'Mentés sablonként',
|
||||
'packing.templateName': 'Sablon neve',
|
||||
'packing.templateSaved': 'Csomaglista elmentve sablonként',
|
||||
'packing.bags': 'Táskák',
|
||||
'packing.noBag': 'Nincs hozzárendelve',
|
||||
'packing.totalWeight': 'Összsúly',
|
||||
@@ -1472,8 +1515,6 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'memories.reviewTitle': 'Nézd át a fotóidat',
|
||||
'memories.reviewHint': 'Kattints a fotókra a megosztásból való kizáráshoz.',
|
||||
'memories.shareCount': '{count} fotó megosztása',
|
||||
'memories.immichUrl': 'Immich szerver URL',
|
||||
'memories.immichApiKey': 'API kulcs',
|
||||
'memories.testConnection': 'Kapcsolat tesztelése',
|
||||
'memories.testFirst': 'Először teszteld a kapcsolatot',
|
||||
'memories.connected': 'Csatlakoztatva',
|
||||
@@ -1700,6 +1741,70 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'notif.generic.text': 'Új értesítésed érkezett',
|
||||
'notif.dev.unknown_event.title': '[DEV] Ismeretlen esemény',
|
||||
'notif.dev.unknown_event.text': 'A(z) "{event}" eseménytípus nincs regisztrálva az EVENT_NOTIFICATION_CONFIG-ban',
|
||||
|
||||
// OAuth scope groups
|
||||
'oauth.scope.group.trips': 'Utazások',
|
||||
'oauth.scope.group.places': 'Helyek',
|
||||
'oauth.scope.group.atlas': 'Atlas',
|
||||
'oauth.scope.group.packing': 'Csomagolás',
|
||||
'oauth.scope.group.todos': 'Feladatok',
|
||||
'oauth.scope.group.budget': 'Költségvetés',
|
||||
'oauth.scope.group.reservations': 'Foglalások',
|
||||
'oauth.scope.group.collab': 'Együttműködés',
|
||||
'oauth.scope.group.notifications': 'Értesítések',
|
||||
'oauth.scope.group.vacay': 'Szabadság',
|
||||
'oauth.scope.group.geo': 'Geo',
|
||||
'oauth.scope.group.weather': 'Időjárás',
|
||||
|
||||
// OAuth scope labels & descriptions
|
||||
'oauth.scope.trips:read.label': 'Utazások és útvonalak megtekintése',
|
||||
'oauth.scope.trips:read.description': 'Utazások, napok, napi feljegyzések és tagok olvasása',
|
||||
'oauth.scope.trips:write.label': 'Utazások és útvonalak szerkesztése',
|
||||
'oauth.scope.trips:write.description': 'Utazások, napok és feljegyzések létrehozása, frissítése és tagok kezelése',
|
||||
'oauth.scope.trips:delete.label': 'Utazások törlése',
|
||||
'oauth.scope.trips:delete.description': 'Teljes utazások végleges törlése — ez a művelet visszafordíthatatlan',
|
||||
'oauth.scope.trips:share.label': 'Megosztási linkek kezelése',
|
||||
'oauth.scope.trips:share.description': 'Nyilvános megosztási linkek létrehozása, frissítése és visszavonása',
|
||||
'oauth.scope.places:read.label': 'Helyek és térképadatok megtekintése',
|
||||
'oauth.scope.places:read.description': 'Helyek, napi hozzárendelések, címkék és kategóriák olvasása',
|
||||
'oauth.scope.places:write.label': 'Helyek kezelése',
|
||||
'oauth.scope.places:write.description': 'Helyek, hozzárendelések és címkék létrehozása, frissítése és törlése',
|
||||
'oauth.scope.atlas:read.label': 'Atlas megtekintése',
|
||||
'oauth.scope.atlas:read.description': 'Meglátogatott országok, régiók és bakancslisták olvasása',
|
||||
'oauth.scope.atlas:write.label': 'Atlas kezelése',
|
||||
'oauth.scope.atlas:write.description': 'Országok és régiók meglátogatottként jelölése, bakancslisták kezelése',
|
||||
'oauth.scope.packing:read.label': 'Csomaglisták megtekintése',
|
||||
'oauth.scope.packing:read.description': 'Csomagolási tételek, táskák és kategória-hozzárendelések olvasása',
|
||||
'oauth.scope.packing:write.label': 'Csomaglisták kezelése',
|
||||
'oauth.scope.packing:write.description': 'Csomagolási tételek és táskák hozzáadása, frissítése, törlése, jelölése és átrendezése',
|
||||
'oauth.scope.todos:read.label': 'Feladatlisták megtekintése',
|
||||
'oauth.scope.todos:read.description': 'Utazás feladatai és kategória-hozzárendelések olvasása',
|
||||
'oauth.scope.todos:write.label': 'Feladatlisták kezelése',
|
||||
'oauth.scope.todos:write.description': 'Feladatok létrehozása, frissítése, jelölése, törlése és átrendezése',
|
||||
'oauth.scope.budget:read.label': 'Költségvetés megtekintése',
|
||||
'oauth.scope.budget:read.description': 'Költségvetési tételek és kiadások részletezésének olvasása',
|
||||
'oauth.scope.budget:write.label': 'Költségvetés kezelése',
|
||||
'oauth.scope.budget:write.description': 'Költségvetési tételek létrehozása, frissítése és törlése',
|
||||
'oauth.scope.reservations:read.label': 'Foglalások megtekintése',
|
||||
'oauth.scope.reservations:read.description': 'Foglalások és szállásadatok olvasása',
|
||||
'oauth.scope.reservations:write.label': 'Foglalások kezelése',
|
||||
'oauth.scope.reservations:write.description': 'Foglalások létrehozása, frissítése, törlése és átrendezése',
|
||||
'oauth.scope.collab:read.label': 'Együttműködés megtekintése',
|
||||
'oauth.scope.collab:read.description': 'Együttműködési feljegyzések, szavazások és üzenetek olvasása',
|
||||
'oauth.scope.collab:write.label': 'Együttműködés kezelése',
|
||||
'oauth.scope.collab:write.description': 'Együttműködési feljegyzések, szavazások és üzenetek létrehozása, frissítése és törlése',
|
||||
'oauth.scope.notifications:read.label': 'Értesítések megtekintése',
|
||||
'oauth.scope.notifications:read.description': 'Alkalmazáson belüli értesítések és olvasatlan számok olvasása',
|
||||
'oauth.scope.notifications:write.label': 'Értesítések kezelése',
|
||||
'oauth.scope.notifications:write.description': 'Értesítések olvasottként jelölése és válaszadás rájuk',
|
||||
'oauth.scope.vacay:read.label': 'Szabadságtervek megtekintése',
|
||||
'oauth.scope.vacay:read.description': 'Szabadságtervezési adatok, bejegyzések és statisztikák olvasása',
|
||||
'oauth.scope.vacay:write.label': 'Szabadságtervek kezelése',
|
||||
'oauth.scope.vacay:write.description': 'Szabadságbejegyzések, ünnepnapok és csapattervek létrehozása és kezelése',
|
||||
'oauth.scope.geo:read.label': 'Térképek és geokódolás',
|
||||
'oauth.scope.geo:read.description': 'Helyek keresése, térkép URL-ek feloldása és koordináták fordított geokódolása',
|
||||
'oauth.scope.weather:read.label': 'Időjárás-előrejelzések',
|
||||
'oauth.scope.weather:read.description': 'Időjárás-előrejelzések lekérése az utazási helyszínekre és dátumokra',
|
||||
}
|
||||
|
||||
export default hu
|
||||
|
||||
@@ -202,6 +202,48 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'settings.mcp.toast.createError': 'Impossibile creare il token',
|
||||
'settings.mcp.toast.deleted': 'Token eliminato',
|
||||
'settings.mcp.toast.deleteError': 'Impossibile eliminare il token',
|
||||
'settings.mcp.apiTokensDeprecated': 'I token API sono deprecati e verranno rimossi in una versione futura. Utilizza invece i client OAuth 2.1.',
|
||||
'settings.oauth.clients': 'Client OAuth 2.1',
|
||||
'settings.oauth.clientsHint': 'Registra client OAuth 2.1 per consentire alle applicazioni MCP di terze parti (Claude Web, Cursor, ecc.) di connettersi senza token statici.',
|
||||
'settings.oauth.createClient': 'Nuovo client',
|
||||
'settings.oauth.noClients': 'Nessun client OAuth registrato.',
|
||||
'settings.oauth.clientId': 'ID client',
|
||||
'settings.oauth.clientSecret': 'Segreto client',
|
||||
'settings.oauth.deleteClient': 'Elimina client',
|
||||
'settings.oauth.deleteClientMessage': 'Questo client e tutte le sessioni attive verranno eliminati definitivamente. Qualsiasi applicazione che lo utilizza perderà immediatamente l\'accesso.',
|
||||
'settings.oauth.rotateSecret': 'Rinnova segreto',
|
||||
'settings.oauth.rotateSecretMessage': 'Verrà generato un nuovo segreto client e tutte le sessioni esistenti verranno invalidate immediatamente. Aggiorna la tua applicazione prima di chiudere questa finestra.',
|
||||
'settings.oauth.rotateSecretConfirm': 'Rinnova',
|
||||
'settings.oauth.rotateSecretConfirming': 'Rinnovo in corso…',
|
||||
'settings.oauth.rotateSecretDoneTitle': 'Nuovo segreto generato',
|
||||
'settings.oauth.rotateSecretDoneWarning': 'Questo segreto viene mostrato una sola volta. Copialo ora e aggiorna la tua applicazione — tutte le sessioni precedenti sono state invalidate.',
|
||||
'settings.oauth.activeSessions': 'Sessioni OAuth attive',
|
||||
'settings.oauth.sessionScopes': 'Ambiti',
|
||||
'settings.oauth.sessionExpires': 'Scade',
|
||||
'settings.oauth.revoke': 'Revoca',
|
||||
'settings.oauth.revokeSession': 'Revoca sessione',
|
||||
'settings.oauth.revokeSessionMessage': 'Questo revocherà immediatamente l\'accesso per questa sessione OAuth.',
|
||||
'settings.oauth.modal.createTitle': 'Registra client OAuth',
|
||||
'settings.oauth.modal.presets': 'Preimpostazioni rapide',
|
||||
'settings.oauth.modal.clientName': 'Nome applicazione',
|
||||
'settings.oauth.modal.clientNamePlaceholder': 'es. Claude Web, La mia app MCP',
|
||||
'settings.oauth.modal.redirectUris': 'URI di reindirizzamento',
|
||||
'settings.oauth.modal.redirectUrisPlaceholder': 'https://your-app.com/callback\nhttps://your-app.com/auth',
|
||||
'settings.oauth.modal.redirectUrisHint': 'Un URI per riga. HTTPS richiesto (localhost esente). Corrispondenza esatta richiesta.',
|
||||
'settings.oauth.modal.scopes': 'Ambiti consentiti',
|
||||
'settings.oauth.modal.scopesHint': 'list_trips e get_trip_summary sono sempre disponibili — nessun ambito richiesto. Permettono all\'IA di scoprire gli ID viaggio necessari.',
|
||||
'settings.oauth.modal.selectAll': 'Seleziona tutto',
|
||||
'settings.oauth.modal.deselectAll': 'Deseleziona tutto',
|
||||
'settings.oauth.modal.creating': 'Registrazione…',
|
||||
'settings.oauth.modal.create': 'Registra client',
|
||||
'settings.oauth.modal.createdTitle': 'Client registrato',
|
||||
'settings.oauth.modal.createdWarning': 'Il segreto client viene mostrato una sola volta. Copialo ora — non può essere recuperato.',
|
||||
'settings.oauth.toast.createError': 'Impossibile registrare il client OAuth',
|
||||
'settings.oauth.toast.deleted': 'Client OAuth eliminato',
|
||||
'settings.oauth.toast.deleteError': 'Impossibile eliminare il client OAuth',
|
||||
'settings.oauth.toast.revoked': 'Sessione revocata',
|
||||
'settings.oauth.toast.revokeError': 'Impossibile revocare la sessione',
|
||||
'settings.oauth.toast.rotateError': 'Impossibile rinnovare il segreto client',
|
||||
'settings.account': 'Account',
|
||||
'settings.about': 'Informazioni',
|
||||
'settings.about.reportBug': 'Segnala un bug',
|
||||
@@ -212,7 +254,7 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'settings.about.description': 'TREK è un pianificatore di viaggi self-hosted che ti aiuta a organizzare i tuoi viaggi dalla prima idea all\'ultimo ricordo. Pianificazione giornaliera, budget, liste bagagli, foto e molto altro — tutto in un unico posto, sul tuo server.',
|
||||
'settings.about.madeWith': 'Fatto con',
|
||||
'settings.about.madeBy': 'da Maurice e una crescente comunità open-source.',
|
||||
'settings.username': 'Username',
|
||||
'settings.username': 'Nome utente',
|
||||
'settings.email': 'Email',
|
||||
'settings.role': 'Ruolo',
|
||||
'settings.roleAdmin': 'Amministratore',
|
||||
@@ -275,9 +317,6 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.notifications.none': 'Disattivato',
|
||||
'admin.notifications.email': 'E-mail (SMTP)',
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'Eventi di notifica',
|
||||
'admin.notifications.eventsHint': 'Scegli quali eventi attivano le notifiche per tutti gli utenti.',
|
||||
'admin.notifications.configureFirst': 'Configura prima le impostazioni SMTP o webhook qui sotto, poi abilita gli eventi.',
|
||||
'admin.notifications.save': 'Salva impostazioni notifiche',
|
||||
'admin.notifications.saved': 'Impostazioni notifiche salvate',
|
||||
'admin.notifications.testWebhook': 'Invia webhook di test',
|
||||
@@ -355,7 +394,7 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'login.hasAccount': 'Hai già un account?',
|
||||
'login.register': 'Registrati',
|
||||
'login.emailPlaceholder': 'tua@email.com',
|
||||
'login.username': 'Username',
|
||||
'login.username': 'Nome utente',
|
||||
'login.oidc.registrationDisabled': 'La registrazione è disabilitata. Contatta il tuo amministratore.',
|
||||
'login.oidc.noEmail': 'Nessuna email ricevuta dal provider.',
|
||||
'login.oidc.tokenFailed': 'Autenticazione fallita.',
|
||||
@@ -1019,6 +1058,7 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'budget.totalBudget': 'Budget totale',
|
||||
'budget.byCategory': 'Per categoria',
|
||||
'budget.editTooltip': 'Clicca per modificare',
|
||||
'budget.linkedToReservation': 'Collegato a una prenotazione — modifica il nome lì',
|
||||
'budget.confirm.deleteCategory': 'Sei sicuro di voler eliminare la categoria "{name}" con {count} voci?',
|
||||
'budget.deleteCategory': 'Elimina categoria',
|
||||
'budget.perPerson': 'Per persona',
|
||||
@@ -1119,6 +1159,9 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'packing.template': 'Modello',
|
||||
'packing.templateApplied': '{count} elementi aggiunti dal modello',
|
||||
'packing.templateError': 'Impossibile applicare il modello',
|
||||
'packing.saveAsTemplate': 'Salva come modello',
|
||||
'packing.templateName': 'Nome modello',
|
||||
'packing.templateSaved': 'Lista bagagli salvata come modello',
|
||||
'packing.bags': 'Valigie',
|
||||
'packing.noBag': 'Non assegnato',
|
||||
'packing.totalWeight': 'Peso totale',
|
||||
@@ -1402,8 +1445,6 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'memories.reviewTitle': 'Rivedi le tue foto',
|
||||
'memories.reviewHint': 'Clicca sulle foto per escluderle dalla condivisione.',
|
||||
'memories.shareCount': 'Condividi {count} foto',
|
||||
'memories.immichUrl': 'URL Server Immich',
|
||||
'memories.immichApiKey': 'Chiave API',
|
||||
'memories.testConnection': 'Test connessione',
|
||||
'memories.testFirst': 'Testa prima la connessione',
|
||||
'memories.connected': 'Connesso',
|
||||
@@ -1661,7 +1702,7 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.notifications.adminWebhookPanel.testFailed': 'Invio webhook di test fallito',
|
||||
'admin.notifications.adminWebhookPanel.alwaysOnHint': 'Il webhook admin si attiva automaticamente quando è configurato un URL',
|
||||
'admin.notifications.adminNotificationsHint': 'Configura quali canali consegnano le notifiche admin (es. avvisi di versione). Il webhook si attiva automaticamente se è impostato un URL webhook admin.',
|
||||
'admin.tabs.notifications': 'Notifications',
|
||||
'admin.tabs.notifications': 'Notifiche',
|
||||
'notifications.versionAvailable.title': 'Aggiornamento disponibile',
|
||||
'notifications.versionAvailable.text': 'TREK {version} è ora disponibile.',
|
||||
'notifications.versionAvailable.button': 'Visualizza dettagli',
|
||||
@@ -1700,6 +1741,70 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'notif.generic.text': 'Hai una nuova notifica',
|
||||
'notif.dev.unknown_event.title': '[DEV] Evento sconosciuto',
|
||||
'notif.dev.unknown_event.text': 'Il tipo di evento "{event}" non è registrato in EVENT_NOTIFICATION_CONFIG',
|
||||
|
||||
// OAuth scope groups
|
||||
'oauth.scope.group.trips': 'Viaggi',
|
||||
'oauth.scope.group.places': 'Luoghi',
|
||||
'oauth.scope.group.atlas': 'Atlas',
|
||||
'oauth.scope.group.packing': 'Bagagli',
|
||||
'oauth.scope.group.todos': 'Attività',
|
||||
'oauth.scope.group.budget': 'Budget',
|
||||
'oauth.scope.group.reservations': 'Prenotazioni',
|
||||
'oauth.scope.group.collab': 'Collaborazione',
|
||||
'oauth.scope.group.notifications': 'Notifiche',
|
||||
'oauth.scope.group.vacay': 'Ferie',
|
||||
'oauth.scope.group.geo': 'Geo',
|
||||
'oauth.scope.group.weather': 'Meteo',
|
||||
|
||||
// OAuth scope labels & descriptions
|
||||
'oauth.scope.trips:read.label': 'Visualizza viaggi e itinerari',
|
||||
'oauth.scope.trips:read.description': 'Leggi viaggi, giorni, note giornaliere e membri',
|
||||
'oauth.scope.trips:write.label': 'Modifica viaggi e itinerari',
|
||||
'oauth.scope.trips:write.description': 'Crea e aggiorna viaggi, giorni, note e gestisci membri',
|
||||
'oauth.scope.trips:delete.label': 'Elimina viaggi',
|
||||
'oauth.scope.trips:delete.description': 'Elimina definitivamente interi viaggi — questa azione è irreversibile',
|
||||
'oauth.scope.trips:share.label': 'Gestisci link di condivisione',
|
||||
'oauth.scope.trips:share.description': 'Crea, aggiorna e revoca link di condivisione pubblici per i viaggi',
|
||||
'oauth.scope.places:read.label': 'Visualizza luoghi e dati mappa',
|
||||
'oauth.scope.places:read.description': 'Leggi luoghi, assegnazioni giornaliere, tag e categorie',
|
||||
'oauth.scope.places:write.label': 'Gestisci luoghi',
|
||||
'oauth.scope.places:write.description': 'Crea, aggiorna ed elimina luoghi, assegnazioni e tag',
|
||||
'oauth.scope.atlas:read.label': 'Visualizza Atlas',
|
||||
'oauth.scope.atlas:read.description': 'Leggi paesi visitati, regioni e lista dei desideri',
|
||||
'oauth.scope.atlas:write.label': 'Gestisci Atlas',
|
||||
'oauth.scope.atlas:write.description': 'Segna paesi e regioni come visitati, gestisci la lista dei desideri',
|
||||
'oauth.scope.packing:read.label': 'Visualizza liste bagagli',
|
||||
'oauth.scope.packing:read.description': 'Leggi articoli, borse e assegnatari di categoria',
|
||||
'oauth.scope.packing:write.label': 'Gestisci liste bagagli',
|
||||
'oauth.scope.packing:write.description': 'Aggiungi, aggiorna, elimina, spunta e riordina articoli e borse',
|
||||
'oauth.scope.todos:read.label': 'Visualizza liste attività',
|
||||
'oauth.scope.todos:read.description': 'Leggi attività del viaggio e assegnatari di categoria',
|
||||
'oauth.scope.todos:write.label': 'Gestisci liste attività',
|
||||
'oauth.scope.todos:write.description': 'Crea, aggiorna, spunta, elimina e riordina attività',
|
||||
'oauth.scope.budget:read.label': 'Visualizza budget',
|
||||
'oauth.scope.budget:read.description': 'Leggi voci di budget e ripartizione delle spese',
|
||||
'oauth.scope.budget:write.label': 'Gestisci budget',
|
||||
'oauth.scope.budget:write.description': 'Crea, aggiorna ed elimina voci di budget',
|
||||
'oauth.scope.reservations:read.label': 'Visualizza prenotazioni',
|
||||
'oauth.scope.reservations:read.description': 'Leggi prenotazioni e dettagli alloggio',
|
||||
'oauth.scope.reservations:write.label': 'Gestisci prenotazioni',
|
||||
'oauth.scope.reservations:write.description': 'Crea, aggiorna, elimina e riordina prenotazioni',
|
||||
'oauth.scope.collab:read.label': 'Visualizza collaborazione',
|
||||
'oauth.scope.collab:read.description': 'Leggi note collaborative, sondaggi e messaggi',
|
||||
'oauth.scope.collab:write.label': 'Gestisci collaborazione',
|
||||
'oauth.scope.collab:write.description': 'Crea, aggiorna ed elimina note collaborative, sondaggi e messaggi',
|
||||
'oauth.scope.notifications:read.label': 'Visualizza notifiche',
|
||||
'oauth.scope.notifications:read.description': 'Leggi notifiche in-app e conteggi non letti',
|
||||
'oauth.scope.notifications:write.label': 'Gestisci notifiche',
|
||||
'oauth.scope.notifications:write.description': 'Segna notifiche come lette e rispondi',
|
||||
'oauth.scope.vacay:read.label': 'Visualizza piani ferie',
|
||||
'oauth.scope.vacay:read.description': 'Leggi dati di pianificazione ferie, voci e statistiche',
|
||||
'oauth.scope.vacay:write.label': 'Gestisci piani ferie',
|
||||
'oauth.scope.vacay:write.description': 'Crea e gestisci voci ferie, festività e piani del team',
|
||||
'oauth.scope.geo:read.label': 'Mappe e geocodifica',
|
||||
'oauth.scope.geo:read.description': 'Cerca luoghi, risolvi URL mappa e geocodifica inversa coordinate',
|
||||
'oauth.scope.weather:read.label': 'Previsioni meteo',
|
||||
'oauth.scope.weather:read.description': 'Ottieni previsioni meteo per luoghi e date del viaggio',
|
||||
}
|
||||
|
||||
export default it
|
||||
|
||||
@@ -179,9 +179,6 @@ const nl: Record<string, string> = {
|
||||
'admin.notifications.none': 'Uitgeschakeld',
|
||||
'admin.notifications.email': 'E-mail (SMTP)',
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'Meldingsgebeurtenissen',
|
||||
'admin.notifications.eventsHint': 'Kies welke gebeurtenissen meldingen activeren voor alle gebruikers.',
|
||||
'admin.notifications.configureFirst': 'Configureer eerst de SMTP- of webhook-instellingen hieronder en schakel dan de events in.',
|
||||
'admin.notifications.save': 'Meldingsinstellingen opslaan',
|
||||
'admin.notifications.saved': 'Meldingsinstellingen opgeslagen',
|
||||
'admin.notifications.testWebhook': 'Testwebhook verzenden',
|
||||
@@ -250,6 +247,48 @@ const nl: Record<string, string> = {
|
||||
'settings.mcp.toast.createError': 'Token aanmaken mislukt',
|
||||
'settings.mcp.toast.deleted': 'Token verwijderd',
|
||||
'settings.mcp.toast.deleteError': 'Token verwijderen mislukt',
|
||||
'settings.mcp.apiTokensDeprecated': 'API-tokens zijn verouderd en worden in een toekomstige versie verwijderd. Gebruik OAuth 2.1-clients in plaats daarvan.',
|
||||
'settings.oauth.clients': 'OAuth 2.1-clients',
|
||||
'settings.oauth.clientsHint': 'Registreer OAuth 2.1-clients zodat externe MCP-toepassingen (Claude Web, Cursor, enz.) verbinding kunnen maken zonder statische tokens.',
|
||||
'settings.oauth.createClient': 'Nieuwe client',
|
||||
'settings.oauth.noClients': 'Geen OAuth-clients geregistreerd.',
|
||||
'settings.oauth.clientId': 'Client-ID',
|
||||
'settings.oauth.clientSecret': 'Clientgeheim',
|
||||
'settings.oauth.deleteClient': 'Client verwijderen',
|
||||
'settings.oauth.deleteClientMessage': 'Deze client en alle actieve sessies worden permanent verwijderd. Elke toepassing die deze client gebruikt, verliest onmiddellijk de toegang.',
|
||||
'settings.oauth.rotateSecret': 'Geheim vernieuwen',
|
||||
'settings.oauth.rotateSecretMessage': 'Er wordt een nieuw clientgeheim gegenereerd en alle bestaande sessies worden direct ongeldig. Werk uw toepassing bij voordat u dit venster sluit.',
|
||||
'settings.oauth.rotateSecretConfirm': 'Vernieuwen',
|
||||
'settings.oauth.rotateSecretConfirming': 'Vernieuwen…',
|
||||
'settings.oauth.rotateSecretDoneTitle': 'Nieuw geheim gegenereerd',
|
||||
'settings.oauth.rotateSecretDoneWarning': 'Dit geheim wordt slechts eenmalig getoond. Kopieer het nu en werk uw toepassing bij — alle vorige sessies zijn ongeldig gemaakt.',
|
||||
'settings.oauth.activeSessions': 'Actieve OAuth-sessies',
|
||||
'settings.oauth.sessionScopes': 'Rechten',
|
||||
'settings.oauth.sessionExpires': 'Verloopt',
|
||||
'settings.oauth.revoke': 'Intrekken',
|
||||
'settings.oauth.revokeSession': 'Sessie intrekken',
|
||||
'settings.oauth.revokeSessionMessage': 'Dit trekt onmiddellijk de toegang voor deze OAuth-sessie in.',
|
||||
'settings.oauth.modal.createTitle': 'OAuth-client registreren',
|
||||
'settings.oauth.modal.presets': 'Snelle instellingen',
|
||||
'settings.oauth.modal.clientName': 'Toepassingsnaam',
|
||||
'settings.oauth.modal.clientNamePlaceholder': 'bijv. Claude Web, Mijn MCP-app',
|
||||
'settings.oauth.modal.redirectUris': 'Redirect-URI\'s',
|
||||
'settings.oauth.modal.redirectUrisPlaceholder': 'https://your-app.com/callback\nhttps://your-app.com/auth',
|
||||
'settings.oauth.modal.redirectUrisHint': 'Eén URI per regel. HTTPS vereist (localhost uitgezonderd). Exacte overeenkomst vereist.',
|
||||
'settings.oauth.modal.scopes': 'Toegestane rechten',
|
||||
'settings.oauth.modal.scopesHint': 'list_trips en get_trip_summary zijn altijd beschikbaar — geen recht vereist. Ze helpen de AI trip-ID\'s te ontdekken.',
|
||||
'settings.oauth.modal.selectAll': 'Alles selecteren',
|
||||
'settings.oauth.modal.deselectAll': 'Alles deselecteren',
|
||||
'settings.oauth.modal.creating': 'Registreren…',
|
||||
'settings.oauth.modal.create': 'Client registreren',
|
||||
'settings.oauth.modal.createdTitle': 'Client geregistreerd',
|
||||
'settings.oauth.modal.createdWarning': 'Het clientgeheim wordt slechts eenmalig getoond. Kopieer het nu — het kan niet worden hersteld.',
|
||||
'settings.oauth.toast.createError': 'OAuth-client kon niet worden geregistreerd',
|
||||
'settings.oauth.toast.deleted': 'OAuth-client verwijderd',
|
||||
'settings.oauth.toast.deleteError': 'OAuth-client kon niet worden verwijderd',
|
||||
'settings.oauth.toast.revoked': 'Sessie ingetrokken',
|
||||
'settings.oauth.toast.revokeError': 'Sessie kon niet worden ingetrokken',
|
||||
'settings.oauth.toast.rotateError': 'Clientgeheim kon niet worden vernieuwd',
|
||||
'settings.account': 'Account',
|
||||
'settings.about': 'Over',
|
||||
'settings.about.reportBug': 'Bug melden',
|
||||
@@ -516,11 +555,11 @@ const nl: Record<string, string> = {
|
||||
'admin.addons.catalog.budget.description': 'Houd uitgaven bij en plan je reisbudget',
|
||||
'admin.addons.catalog.documents.name': 'Documenten',
|
||||
'admin.addons.catalog.documents.description': 'Bewaar en beheer reisdocumenten',
|
||||
'admin.addons.catalog.vacay.name': 'Vacay',
|
||||
'admin.addons.catalog.vacay.name': 'Vakantie',
|
||||
'admin.addons.catalog.vacay.description': 'Persoonlijke vakantieplanner met kalenderweergave',
|
||||
'admin.addons.catalog.atlas.name': 'Atlas',
|
||||
'admin.addons.catalog.atlas.description': 'Wereldkaart met bezochte landen en reisstatistieken',
|
||||
'admin.addons.catalog.collab.name': 'Collab',
|
||||
'admin.addons.catalog.collab.name': 'Samenwerking',
|
||||
'admin.addons.catalog.collab.description': 'Realtime notities, polls en chat voor het plannen van reizen',
|
||||
'admin.addons.subtitleBefore': 'Schakel functies in of uit om je ',
|
||||
'admin.addons.subtitleAfter': '-ervaring aan te passen.',
|
||||
@@ -744,7 +783,7 @@ const nl: Record<string, string> = {
|
||||
'atlas.placeVisited': 'Bezochte plaats',
|
||||
'atlas.placesVisited': 'Bezochte plaatsen',
|
||||
'atlas.statsTab': 'Statistieken',
|
||||
'atlas.bucketTab': 'Bucket List',
|
||||
'atlas.bucketTab': 'Bucketlist',
|
||||
'atlas.addBucket': 'Toevoegen aan bucket list',
|
||||
'atlas.bucketNamePlaceholder': 'Plaats of bestemming...',
|
||||
'atlas.bucketNotesPlaceholder': 'Notities (optioneel)',
|
||||
@@ -855,7 +894,7 @@ const nl: Record<string, string> = {
|
||||
'places.noCategory': 'Geen categorie',
|
||||
'places.categoryNamePlaceholder': 'Categorienaam',
|
||||
'places.formTime': 'Tijd',
|
||||
'places.startTime': 'Start',
|
||||
'places.startTime': 'Starttijd',
|
||||
'places.endTime': 'Einde',
|
||||
'places.endTimeBeforeStart': 'Eindtijd is vóór de starttijd',
|
||||
'places.timeCollision': 'Tijdoverlap met:',
|
||||
@@ -871,7 +910,7 @@ const nl: Record<string, string> = {
|
||||
'places.nameRequired': 'Voer een naam in',
|
||||
'places.saveError': 'Opslaan mislukt',
|
||||
// Place Inspector
|
||||
'inspector.opened': 'Open',
|
||||
'inspector.opened': 'Openingstijden',
|
||||
'inspector.closed': 'Gesloten',
|
||||
'inspector.openingHours': 'Openingstijden',
|
||||
'inspector.showHours': 'Openingstijden tonen',
|
||||
@@ -917,8 +956,8 @@ const nl: Record<string, string> = {
|
||||
'reservations.meta.trainNumber': 'Treinnr.',
|
||||
'reservations.meta.platform': 'Perron',
|
||||
'reservations.meta.seat': 'Stoel',
|
||||
'reservations.meta.checkIn': 'Check-in',
|
||||
'reservations.meta.checkOut': 'Check-out',
|
||||
'reservations.meta.checkIn': 'Inchecken',
|
||||
'reservations.meta.checkOut': 'Uitchecken',
|
||||
'reservations.meta.linkAccommodation': 'Accommodatie',
|
||||
'reservations.meta.pickAccommodation': 'Koppel aan accommodatie',
|
||||
'reservations.meta.noAccommodation': 'Geen',
|
||||
@@ -1018,11 +1057,12 @@ const nl: Record<string, string> = {
|
||||
'budget.totalBudget': 'Totaal budget',
|
||||
'budget.byCategory': 'Per categorie',
|
||||
'budget.editTooltip': 'Klik om te bewerken',
|
||||
'budget.linkedToReservation': 'Gekoppeld aan een reservering — bewerk de naam daar',
|
||||
'budget.confirm.deleteCategory': 'Weet je zeker dat je de categorie "{name}" met {count} invoeren wilt verwijderen?',
|
||||
'budget.deleteCategory': 'Categorie verwijderen',
|
||||
'budget.perPerson': 'Per persoon',
|
||||
'budget.paid': 'Betaald',
|
||||
'budget.open': 'Open',
|
||||
'budget.open': 'Openstaand',
|
||||
'budget.noMembers': 'Geen leden toegewezen',
|
||||
'budget.settlement': 'Afrekening',
|
||||
'budget.settlementInfo': 'Klik op de avatar van een lid bij een budgetpost om deze groen te markeren — dit betekent dat diegene heeft betaald. De afrekening toont vervolgens wie wie hoeveel verschuldigd is.',
|
||||
@@ -1099,7 +1139,7 @@ const nl: Record<string, string> = {
|
||||
'packing.addPlaceholder': 'Nieuw item toevoegen...',
|
||||
'packing.categoryPlaceholder': 'Categorie...',
|
||||
'packing.filterAll': 'Alle',
|
||||
'packing.filterOpen': 'Open',
|
||||
'packing.filterOpen': 'Openstaand',
|
||||
'packing.filterDone': 'Klaar',
|
||||
'packing.emptyTitle': 'Paklijst is leeg',
|
||||
'packing.emptyHint': 'Voeg items toe of gebruik de suggesties',
|
||||
@@ -1108,6 +1148,7 @@ const nl: Record<string, string> = {
|
||||
'packing.menuCheckAll': 'Alles aanvinken',
|
||||
'packing.menuUncheckAll': 'Alles uitvinken',
|
||||
'packing.menuDeleteCat': 'Categorie verwijderen',
|
||||
'packing.assignUser': 'Gebruiker toewijzen',
|
||||
'packing.addItem': 'Item toevoegen',
|
||||
'packing.addItemPlaceholder': 'Itemnaam...',
|
||||
'packing.addCategory': 'Categorie toevoegen',
|
||||
@@ -1116,7 +1157,9 @@ const nl: Record<string, string> = {
|
||||
'packing.template': 'Sjabloon',
|
||||
'packing.templateApplied': '{count} items toegevoegd vanuit sjabloon',
|
||||
'packing.templateError': 'Fout bij toepassen van sjabloon',
|
||||
'packing.assignUser': 'Gebruiker toewijzen',
|
||||
'packing.saveAsTemplate': 'Opslaan als sjabloon',
|
||||
'packing.templateName': 'Sjabloonnaam',
|
||||
'packing.templateSaved': 'Paklijst opgeslagen als sjabloon',
|
||||
'packing.noMembers': 'Geen leden',
|
||||
'packing.bags': 'Bagage',
|
||||
'packing.noBag': 'Niet toegewezen',
|
||||
@@ -1381,8 +1424,8 @@ const nl: Record<string, string> = {
|
||||
'day.hotelDayRange': 'Toepassen op dagen',
|
||||
'day.noPlacesForHotel': 'Voeg eerst plaatsen toe aan je reis',
|
||||
'day.allDays': 'Alle',
|
||||
'day.checkIn': 'Check-in',
|
||||
'day.checkOut': 'Check-out',
|
||||
'day.checkIn': 'Inchecken',
|
||||
'day.checkOut': 'Uitchecken',
|
||||
'day.confirmation': 'Bevestiging',
|
||||
'day.editAccommodation': 'Accommodatie bewerken',
|
||||
'day.reservations': 'Reserveringen',
|
||||
@@ -1401,8 +1444,6 @@ const nl: Record<string, string> = {
|
||||
'memories.reviewTitle': 'Je foto\'s bekijken',
|
||||
'memories.reviewHint': 'Klik op foto\'s om ze uit te sluiten van delen.',
|
||||
'memories.shareCount': '{count} foto\'s delen',
|
||||
'memories.immichUrl': 'Immich Server URL',
|
||||
'memories.immichApiKey': 'API-sleutel',
|
||||
'memories.testConnection': 'Verbinding testen',
|
||||
'memories.testFirst': 'Test eerst de verbinding',
|
||||
'memories.connected': 'Verbonden',
|
||||
@@ -1606,7 +1647,7 @@ const nl: Record<string, string> = {
|
||||
'todo.subtab.todo': 'Taken',
|
||||
'todo.completed': 'voltooid',
|
||||
'todo.filter.all': 'Alles',
|
||||
'todo.filter.open': 'Open',
|
||||
'todo.filter.open': 'Openstaand',
|
||||
'todo.filter.done': 'Klaar',
|
||||
'todo.uncategorized': 'Zonder categorie',
|
||||
'todo.namePlaceholder': 'Taaknaam',
|
||||
@@ -1699,6 +1740,70 @@ const nl: Record<string, string> = {
|
||||
'notif.generic.text': 'Je hebt een nieuwe melding',
|
||||
'notif.dev.unknown_event.title': '[DEV] Onbekende gebeurtenis',
|
||||
'notif.dev.unknown_event.text': 'Gebeurtenistype "{event}" is niet geregistreerd in EVENT_NOTIFICATION_CONFIG',
|
||||
|
||||
// OAuth scope groups
|
||||
'oauth.scope.group.trips': 'Reizen',
|
||||
'oauth.scope.group.places': 'Plaatsen',
|
||||
'oauth.scope.group.atlas': 'Atlas',
|
||||
'oauth.scope.group.packing': 'Paklijst',
|
||||
'oauth.scope.group.todos': 'Taken',
|
||||
'oauth.scope.group.budget': 'Budget',
|
||||
'oauth.scope.group.reservations': 'Reserveringen',
|
||||
'oauth.scope.group.collab': 'Samenwerking',
|
||||
'oauth.scope.group.notifications': 'Meldingen',
|
||||
'oauth.scope.group.vacay': 'Vakantie',
|
||||
'oauth.scope.group.geo': 'Geo',
|
||||
'oauth.scope.group.weather': 'Weer',
|
||||
|
||||
// OAuth scope labels & descriptions
|
||||
'oauth.scope.trips:read.label': 'Reizen en reisplannen bekijken',
|
||||
'oauth.scope.trips:read.description': 'Reizen, dagen, notities en leden lezen',
|
||||
'oauth.scope.trips:write.label': 'Reizen en reisplannen bewerken',
|
||||
'oauth.scope.trips:write.description': 'Reizen, dagen en notities aanmaken, bijwerken en leden beheren',
|
||||
'oauth.scope.trips:delete.label': 'Reizen verwijderen',
|
||||
'oauth.scope.trips:delete.description': 'Hele reizen permanent verwijderen — deze actie is onomkeerbaar',
|
||||
'oauth.scope.trips:share.label': 'Deellinks beheren',
|
||||
'oauth.scope.trips:share.description': 'Publieke deellinks aanmaken, bijwerken en intrekken',
|
||||
'oauth.scope.places:read.label': 'Plaatsen en kaartgegevens bekijken',
|
||||
'oauth.scope.places:read.description': 'Plaatsen, dagtoewijzingen, tags en categorieën lezen',
|
||||
'oauth.scope.places:write.label': 'Plaatsen beheren',
|
||||
'oauth.scope.places:write.description': 'Plaatsen, toewijzingen en tags aanmaken, bijwerken en verwijderen',
|
||||
'oauth.scope.atlas:read.label': 'Atlas bekijken',
|
||||
'oauth.scope.atlas:read.description': 'Bezochte landen, regio\'s en bucketlist lezen',
|
||||
'oauth.scope.atlas:write.label': 'Atlas beheren',
|
||||
'oauth.scope.atlas:write.description': 'Landen en regio\'s markeren als bezocht, bucketlist beheren',
|
||||
'oauth.scope.packing:read.label': 'Paklijsten bekijken',
|
||||
'oauth.scope.packing:read.description': 'Pakartikelen, tassen en categorietoewijzingen lezen',
|
||||
'oauth.scope.packing:write.label': 'Paklijsten beheren',
|
||||
'oauth.scope.packing:write.description': 'Pakartikelen en tassen toevoegen, bijwerken, verwijderen, omschakelen en herordenen',
|
||||
'oauth.scope.todos:read.label': 'Takenlijsten bekijken',
|
||||
'oauth.scope.todos:read.description': 'Reistaakitems en categorietoewijzingen lezen',
|
||||
'oauth.scope.todos:write.label': 'Takenlijsten beheren',
|
||||
'oauth.scope.todos:write.description': 'Taakitems aanmaken, bijwerken, omschakelen, verwijderen en herordenen',
|
||||
'oauth.scope.budget:read.label': 'Budget bekijken',
|
||||
'oauth.scope.budget:read.description': 'Budgetitems en kostenspecificatie lezen',
|
||||
'oauth.scope.budget:write.label': 'Budget beheren',
|
||||
'oauth.scope.budget:write.description': 'Budgetitems aanmaken, bijwerken en verwijderen',
|
||||
'oauth.scope.reservations:read.label': 'Reserveringen bekijken',
|
||||
'oauth.scope.reservations:read.description': 'Reserveringen en accommodatiedetails lezen',
|
||||
'oauth.scope.reservations:write.label': 'Reserveringen beheren',
|
||||
'oauth.scope.reservations:write.description': 'Reserveringen aanmaken, bijwerken, verwijderen en herordenen',
|
||||
'oauth.scope.collab:read.label': 'Samenwerking bekijken',
|
||||
'oauth.scope.collab:read.description': 'Samenwerkingsnotities, polls en berichten lezen',
|
||||
'oauth.scope.collab:write.label': 'Samenwerking beheren',
|
||||
'oauth.scope.collab:write.description': 'Samenwerkingsnotities, polls en berichten aanmaken, bijwerken en verwijderen',
|
||||
'oauth.scope.notifications:read.label': 'Meldingen bekijken',
|
||||
'oauth.scope.notifications:read.description': 'In-app meldingen en ongelezen aantallen lezen',
|
||||
'oauth.scope.notifications:write.label': 'Meldingen beheren',
|
||||
'oauth.scope.notifications:write.description': 'Meldingen als gelezen markeren en erop reageren',
|
||||
'oauth.scope.vacay:read.label': 'Vakantieplannen bekijken',
|
||||
'oauth.scope.vacay:read.description': 'Vakantieplanningsgegevens, invoeren en statistieken lezen',
|
||||
'oauth.scope.vacay:write.label': 'Vakantieplannen beheren',
|
||||
'oauth.scope.vacay:write.description': 'Vakantie-invoeren, feestdagen en teamplannen aanmaken en beheren',
|
||||
'oauth.scope.geo:read.label': 'Kaarten & geocodering',
|
||||
'oauth.scope.geo:read.description': 'Locaties zoeken, kaart-URL\'s oplossen en coördinaten omgekeerd geocoderen',
|
||||
'oauth.scope.weather:read.label': 'Weersverwachtingen',
|
||||
'oauth.scope.weather:read.description': 'Weersverwachtingen ophalen voor reislocaties en -datums',
|
||||
}
|
||||
|
||||
export default nl
|
||||
|
||||
@@ -29,7 +29,7 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'common.change': 'Zmień',
|
||||
'common.uploading': 'Przesyłanie...',
|
||||
'common.backToPlanning': 'Powrót do planowania',
|
||||
'common.reset': 'Reset',
|
||||
'common.reset': 'Resetuj',
|
||||
|
||||
// Navbar
|
||||
'nav.trip': 'Podróż',
|
||||
@@ -198,7 +198,7 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'settings.mcp.endpoint': 'Endpoint MCP',
|
||||
'settings.mcp.clientConfig': 'Konfiguracja klienta',
|
||||
'settings.mcp.clientConfigHint': 'Zastąp <your_token> tokenem API z listy poniżej. Ścieżka do npx może wymagać dostosowania do Twojego systemu (np. C:\\PROGRA~1\\nodejs\\npx.cmd w systemie Windows).',
|
||||
'settings.mcp.clientConfigHintOAuth': 'Replace <your_client_id> and <your_client_secret> with the credentials shown in the OAuth 2.1 client you created above. mcp-remote will open your browser to complete the authorization the first time you connect. The path to npx may need to be adjusted for your system (e.g. C:\PROGRA~1\nodejs\npx.cmd on Windows).',
|
||||
'settings.mcp.clientConfigHintOAuth': 'Zastąp <your_client_id> i <your_client_secret> danymi uwierzytelniającymi z klienta OAuth 2.1 utworzonego powyżej. mcp-remote otworzy przeglądarkę, aby dokończyć autoryzację przy pierwszym połączeniu. Ścieżka do npx może wymagać dostosowania do Twojego systemu (np. C:\\PROGRA~1\\nodejs\\npx.cmd w systemie Windows).',
|
||||
'settings.mcp.copy': 'Kopiuj',
|
||||
'settings.mcp.copied': 'Skopiowano!',
|
||||
'settings.mcp.apiTokens': 'Tokeny API',
|
||||
@@ -220,6 +220,48 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'settings.mcp.toast.createError': 'Nie udało się utworzyć tokenu',
|
||||
'settings.mcp.toast.deleted': 'Token został usunięty',
|
||||
'settings.mcp.toast.deleteError': 'Nie udało się usunąć tokenu',
|
||||
'settings.mcp.apiTokensDeprecated': 'Tokeny API są przestarzałe i zostaną usunięte w przyszłej wersji. Użyj zamiast tego klientów OAuth 2.1.',
|
||||
'settings.oauth.clients': 'Klienci OAuth 2.1',
|
||||
'settings.oauth.clientsHint': 'Zarejestruj klientów OAuth 2.1, aby zewnętrzne aplikacje MCP (Claude Web, Cursor itp.) mogły się łączyć bez statycznych tokenów.',
|
||||
'settings.oauth.createClient': 'Nowy klient',
|
||||
'settings.oauth.noClients': 'Brak zarejestrowanych klientów OAuth.',
|
||||
'settings.oauth.clientId': 'ID klienta',
|
||||
'settings.oauth.clientSecret': 'Sekret klienta',
|
||||
'settings.oauth.deleteClient': 'Usuń klienta',
|
||||
'settings.oauth.deleteClientMessage': 'Ten klient i wszystkie aktywne sesje zostaną trwale usunięte. Każda aplikacja, która go używa, natychmiast utraci dostęp.',
|
||||
'settings.oauth.rotateSecret': 'Odnów sekret',
|
||||
'settings.oauth.rotateSecretMessage': 'Zostanie wygenerowany nowy sekret klienta, a wszystkie istniejące sesje zostaną natychmiast unieważnione. Zaktualizuj aplikację przed zamknięciem tego okna.',
|
||||
'settings.oauth.rotateSecretConfirm': 'Odnów',
|
||||
'settings.oauth.rotateSecretConfirming': 'Odnawianie…',
|
||||
'settings.oauth.rotateSecretDoneTitle': 'Wygenerowano nowy sekret',
|
||||
'settings.oauth.rotateSecretDoneWarning': 'Ten sekret jest wyświetlany tylko raz. Skopiuj go teraz i zaktualizuj aplikację — wszystkie poprzednie sesje zostały unieważnione.',
|
||||
'settings.oauth.activeSessions': 'Aktywne sesje OAuth',
|
||||
'settings.oauth.sessionScopes': 'Uprawnienia',
|
||||
'settings.oauth.sessionExpires': 'Wygasa',
|
||||
'settings.oauth.revoke': 'Unieważnij',
|
||||
'settings.oauth.revokeSession': 'Unieważnij sesję',
|
||||
'settings.oauth.revokeSessionMessage': 'Spowoduje to natychmiastowe unieważnienie dostępu dla tej sesji OAuth.',
|
||||
'settings.oauth.modal.createTitle': 'Zarejestruj klienta OAuth',
|
||||
'settings.oauth.modal.presets': 'Szybkie ustawienia',
|
||||
'settings.oauth.modal.clientName': 'Nazwa aplikacji',
|
||||
'settings.oauth.modal.clientNamePlaceholder': 'np. Claude Web, Moja aplikacja MCP',
|
||||
'settings.oauth.modal.redirectUris': 'URI przekierowania',
|
||||
'settings.oauth.modal.redirectUrisPlaceholder': 'https://your-app.com/callback\nhttps://your-app.com/auth',
|
||||
'settings.oauth.modal.redirectUrisHint': 'Jeden URI na linię. Wymagane HTTPS (localhost zwolniony). Wymagana dokładna zgodność.',
|
||||
'settings.oauth.modal.scopes': 'Dozwolone uprawnienia',
|
||||
'settings.oauth.modal.scopesHint': 'list_trips i get_trip_summary są zawsze dostępne — bez wymaganych uprawnień. Umożliwiają AI odkrycie potrzebnych ID podróży.',
|
||||
'settings.oauth.modal.selectAll': 'Zaznacz wszystko',
|
||||
'settings.oauth.modal.deselectAll': 'Odznacz wszystko',
|
||||
'settings.oauth.modal.creating': 'Rejestrowanie…',
|
||||
'settings.oauth.modal.create': 'Zarejestruj klienta',
|
||||
'settings.oauth.modal.createdTitle': 'Klient zarejestrowany',
|
||||
'settings.oauth.modal.createdWarning': 'Sekret klienta jest wyświetlany tylko raz. Skopiuj go teraz — nie można go odzyskać.',
|
||||
'settings.oauth.toast.createError': 'Nie udało się zarejestrować klienta OAuth',
|
||||
'settings.oauth.toast.deleted': 'Klient OAuth usunięty',
|
||||
'settings.oauth.toast.deleteError': 'Nie udało się usunąć klienta OAuth',
|
||||
'settings.oauth.toast.revoked': 'Sesja unieważniona',
|
||||
'settings.oauth.toast.revokeError': 'Nie udało się unieważnić sesji',
|
||||
'settings.oauth.toast.rotateError': 'Nie udało się odnowić sekretu klienta',
|
||||
'settings.account': 'Konto',
|
||||
'settings.about': 'O aplikacji',
|
||||
'settings.about.reportBug': 'Zgłoś błąd',
|
||||
@@ -429,7 +471,7 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.recommended': 'Polecane',
|
||||
'admin.weatherKey': 'Klucz OpenWeatherMap API',
|
||||
'admin.weatherKeyHint': 'Do danych pogodowych. Uzyskaj go bezpłatnie na openweathermap.org',
|
||||
'admin.validateKey': 'Test',
|
||||
'admin.validateKey': 'Testuj',
|
||||
'admin.keyValid': 'Połączono',
|
||||
'admin.keyInvalid': 'Niepoprawny',
|
||||
'admin.keySaved': 'Klucze API zostały zapisane',
|
||||
@@ -485,7 +527,7 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.addons.catalog.vacay.description': 'Osobisty planer urlopu z widokiem kalendarza',
|
||||
'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': 'Collab',
|
||||
'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.memories.name': 'Zdjęcia (Immich)',
|
||||
'admin.addons.catalog.memories.description': 'Udostępniaj zdjęcia z podróży za pośrednictwem swojej instancji Immich',
|
||||
@@ -610,7 +652,7 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'vacay.legend': 'Legenda',
|
||||
'vacay.publicHoliday': 'Święto państwowe',
|
||||
'vacay.companyHoliday': 'Urlop firmowy',
|
||||
'vacay.weekend': 'Weekend',
|
||||
'vacay.weekend': 'Weekendowy',
|
||||
'vacay.modeVacation': 'Urlop',
|
||||
'vacay.modeCompany': 'Urlop firmowy',
|
||||
'vacay.entitlement': 'Wymiar',
|
||||
@@ -708,7 +750,7 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'atlas.lastTrip': 'Ostatnia podróż',
|
||||
'atlas.nextTrip': 'Następna podróż',
|
||||
'atlas.daysLeft': 'dni do wyjazdu',
|
||||
'atlas.streak': 'Streak',
|
||||
'atlas.streak': 'Seria',
|
||||
'atlas.years': 'lata',
|
||||
'atlas.yearInRow': 'rok z rzędu',
|
||||
'atlas.yearsInRow': 'lat z rzędu',
|
||||
@@ -974,6 +1016,7 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'budget.totalBudget': 'Całkowity budżet',
|
||||
'budget.byCategory': 'Według kategorii',
|
||||
'budget.editTooltip': 'Kliknij, aby edytować',
|
||||
'budget.linkedToReservation': 'Powiązano z rezerwacją — edytuj nazwę tam',
|
||||
'budget.confirm.deleteCategory': 'Czy na pewno chcesz usunąć kategorię "{name}" z {count} wpisami?',
|
||||
'budget.deleteCategory': 'Usuń kategorię',
|
||||
'budget.perPerson': 'Za osobę',
|
||||
@@ -1074,6 +1117,9 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'packing.template': 'Szablon',
|
||||
'packing.templateApplied': '{count} przedmiotów dodanych z szablonu',
|
||||
'packing.templateError': 'Nie udało się zastosować szablonu',
|
||||
'packing.saveAsTemplate': 'Zapisz jako szablon',
|
||||
'packing.templateName': 'Nazwa szablonu',
|
||||
'packing.templateSaved': 'Lista pakowania zapisana jako szablon',
|
||||
'packing.bags': 'Torby',
|
||||
'packing.noBag': 'Nieprzypisane',
|
||||
'packing.totalWeight': 'Waga całkowita',
|
||||
@@ -1357,8 +1403,6 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'memories.reviewTitle': 'Przejrzyj swoje zdjęcia',
|
||||
'memories.reviewHint': 'Kliknij w zdjęcia, aby wykluczyć je z udostępnienia.',
|
||||
'memories.shareCount': 'Udostępnij {count} zdjęć',
|
||||
'memories.immichUrl': 'URL serwera Immich',
|
||||
'memories.immichApiKey': 'Klucz API',
|
||||
'memories.testConnection': 'Test',
|
||||
'memories.connected': 'Połączono',
|
||||
'memories.disconnected': 'Nie połączono',
|
||||
@@ -1467,11 +1511,8 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.notifications.title': 'Powiadomienia',
|
||||
'admin.notifications.hint': 'Wybierz jeden kanał powiadomień.',
|
||||
'admin.notifications.none': 'Wyłączone',
|
||||
'admin.notifications.email': 'Email (SMTP)',
|
||||
'admin.notifications.email': 'E-mail (SMTP)',
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'Zdarzenia powiadomień',
|
||||
'admin.notifications.eventsHint': 'Wybierz zdarzenia wyzwalające powiadomienia.',
|
||||
'admin.notifications.configureFirst': 'Najpierw skonfiguruj ustawienia SMTP lub webhook.',
|
||||
'admin.notifications.save': 'Zapisz ustawienia powiadomień',
|
||||
'admin.notifications.saved': 'Ustawienia powiadomień zapisane',
|
||||
'admin.notifications.testWebhook': 'Wyślij testowy webhook',
|
||||
@@ -1496,7 +1537,7 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'settings.webhookUrl.hint': 'Wprowadź adres URL webhooka Discord, Slack lub własnego, aby otrzymywać powiadomienia.',
|
||||
'settings.webhookUrl.save': 'Zapisz',
|
||||
'settings.webhookUrl.saved': 'URL webhooka zapisany',
|
||||
'settings.webhookUrl.test': 'Test',
|
||||
'settings.webhookUrl.test': 'Testuj',
|
||||
'settings.webhookUrl.testSuccess': 'Testowy webhook wysłany pomyślnie',
|
||||
'settings.webhookUrl.testFailed': 'Wysyłanie testowego webhooka nie powiodło się',
|
||||
'settings.notificationPreferences.inapp': 'In-App',
|
||||
@@ -1692,6 +1733,70 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'notif.generic.text': 'Masz nowe powiadomienie',
|
||||
'notif.dev.unknown_event.title': '[DEV] Nieznane zdarzenie',
|
||||
'notif.dev.unknown_event.text': 'Typ zdarzenia "{event}" nie jest zarejestrowany w EVENT_NOTIFICATION_CONFIG',
|
||||
|
||||
// OAuth scope groups
|
||||
'oauth.scope.group.trips': 'Podróże',
|
||||
'oauth.scope.group.places': 'Miejsca',
|
||||
'oauth.scope.group.atlas': 'Atlas',
|
||||
'oauth.scope.group.packing': 'Pakowanie',
|
||||
'oauth.scope.group.todos': 'Zadania',
|
||||
'oauth.scope.group.budget': 'Budżet',
|
||||
'oauth.scope.group.reservations': 'Rezerwacje',
|
||||
'oauth.scope.group.collab': 'Współpraca',
|
||||
'oauth.scope.group.notifications': 'Powiadomienia',
|
||||
'oauth.scope.group.vacay': 'Urlop',
|
||||
'oauth.scope.group.geo': 'Geo',
|
||||
'oauth.scope.group.weather': 'Pogoda',
|
||||
|
||||
// OAuth scope labels & descriptions
|
||||
'oauth.scope.trips:read.label': 'Przeglądaj podróże i itineraria',
|
||||
'oauth.scope.trips:read.description': 'Odczytuj podróże, dni, notatki i członków',
|
||||
'oauth.scope.trips:write.label': 'Edytuj podróże i itineraria',
|
||||
'oauth.scope.trips:write.description': 'Twórz i aktualizuj podróże, dni, notatki oraz zarządzaj członkami',
|
||||
'oauth.scope.trips:delete.label': 'Usuń podróże',
|
||||
'oauth.scope.trips:delete.description': 'Trwale usuń całe podróże — ta akcja jest nieodwracalna',
|
||||
'oauth.scope.trips:share.label': 'Zarządzaj linkami udostępniania',
|
||||
'oauth.scope.trips:share.description': 'Twórz, aktualizuj i unieważniaj publiczne linki udostępniania',
|
||||
'oauth.scope.places:read.label': 'Przeglądaj miejsca i dane mapy',
|
||||
'oauth.scope.places:read.description': 'Odczytuj miejsca, przypisania dni, tagi i kategorie',
|
||||
'oauth.scope.places:write.label': 'Zarządzaj miejscami',
|
||||
'oauth.scope.places:write.description': 'Twórz, aktualizuj i usuń miejsca, przypisania i tagi',
|
||||
'oauth.scope.atlas:read.label': 'Przeglądaj Atlas',
|
||||
'oauth.scope.atlas:read.description': 'Odczytuj odwiedzone kraje, regiony i listę marzeń',
|
||||
'oauth.scope.atlas:write.label': 'Zarządzaj Atlasem',
|
||||
'oauth.scope.atlas:write.description': 'Oznaczaj kraje i regiony jako odwiedzone, zarządzaj listą marzeń',
|
||||
'oauth.scope.packing:read.label': 'Przeglądaj listy pakowania',
|
||||
'oauth.scope.packing:read.description': 'Odczytuj przedmioty, torby i przypisania kategorii',
|
||||
'oauth.scope.packing:write.label': 'Zarządzaj listami pakowania',
|
||||
'oauth.scope.packing:write.description': 'Dodawaj, aktualizuj, usuwaj, zaznaczaj i porządkuj przedmioty i torby',
|
||||
'oauth.scope.todos:read.label': 'Przeglądaj listy zadań',
|
||||
'oauth.scope.todos:read.description': 'Odczytuj zadania podróży i przypisania kategorii',
|
||||
'oauth.scope.todos:write.label': 'Zarządzaj listami zadań',
|
||||
'oauth.scope.todos:write.description': 'Twórz, aktualizuj, zaznaczaj, usuwaj i porządkuj zadania',
|
||||
'oauth.scope.budget:read.label': 'Przeglądaj budżet',
|
||||
'oauth.scope.budget:read.description': 'Odczytuj pozycje budżetu i zestawienie wydatków',
|
||||
'oauth.scope.budget:write.label': 'Zarządzaj budżetem',
|
||||
'oauth.scope.budget:write.description': 'Twórz, aktualizuj i usuń pozycje budżetu',
|
||||
'oauth.scope.reservations:read.label': 'Przeglądaj rezerwacje',
|
||||
'oauth.scope.reservations:read.description': 'Odczytuj rezerwacje i szczegóły zakwaterowania',
|
||||
'oauth.scope.reservations:write.label': 'Zarządzaj rezerwacjami',
|
||||
'oauth.scope.reservations:write.description': 'Twórz, aktualizuj, usuwaj i porządkuj rezerwacje',
|
||||
'oauth.scope.collab:read.label': 'Przeglądaj współpracę',
|
||||
'oauth.scope.collab:read.description': 'Odczytuj notatki, ankiety i wiadomości',
|
||||
'oauth.scope.collab:write.label': 'Zarządzaj współpracą',
|
||||
'oauth.scope.collab:write.description': 'Twórz, aktualizuj i usuń notatki, ankiety i wiadomości',
|
||||
'oauth.scope.notifications:read.label': 'Przeglądaj powiadomienia',
|
||||
'oauth.scope.notifications:read.description': 'Odczytuj powiadomienia i liczby nieprzeczytanych',
|
||||
'oauth.scope.notifications:write.label': 'Zarządzaj powiadomieniami',
|
||||
'oauth.scope.notifications:write.description': 'Oznaczaj powiadomienia jako przeczytane i odpowiadaj na nie',
|
||||
'oauth.scope.vacay:read.label': 'Przeglądaj plany urlopowe',
|
||||
'oauth.scope.vacay:read.description': 'Odczytuj dane planowania urlopu, wpisy i statystyki',
|
||||
'oauth.scope.vacay:write.label': 'Zarządzaj planami urlopowymi',
|
||||
'oauth.scope.vacay:write.description': 'Twórz i zarządzaj wpisami urlopowymi, świętami i planami zespołu',
|
||||
'oauth.scope.geo:read.label': 'Mapy i geokodowanie',
|
||||
'oauth.scope.geo:read.description': 'Wyszukuj miejsca, rozwiązuj adresy URL map i odwrotnie geokoduj współrzędne',
|
||||
'oauth.scope.weather:read.label': 'Prognozy pogody',
|
||||
'oauth.scope.weather:read.description': 'Pobieraj prognozy pogody dla miejsc i dat podróży',
|
||||
}
|
||||
|
||||
export default pl
|
||||
|
||||
@@ -179,9 +179,6 @@ const ru: Record<string, string> = {
|
||||
'admin.notifications.none': 'Отключено',
|
||||
'admin.notifications.email': 'Эл. почта (SMTP)',
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'События уведомлений',
|
||||
'admin.notifications.eventsHint': 'Выберите, какие события вызывают уведомления для всех пользователей.',
|
||||
'admin.notifications.configureFirst': 'Сначала настройте SMTP или webhook ниже, затем включите события.',
|
||||
'admin.notifications.save': 'Сохранить настройки уведомлений',
|
||||
'admin.notifications.saved': 'Настройки уведомлений сохранены',
|
||||
'admin.notifications.testWebhook': 'Отправить тестовый вебхук',
|
||||
@@ -228,7 +225,7 @@ const ru: Record<string, string> = {
|
||||
'settings.mcp.endpoint': 'MCP-эндпоинт',
|
||||
'settings.mcp.clientConfig': 'Конфигурация клиента',
|
||||
'settings.mcp.clientConfigHint': 'Замените <your_token> на API-токен из списка ниже. Путь к npx может потребовать настройки для вашей системы (например, C:\\PROGRA~1\\nodejs\\npx.cmd в Windows).',
|
||||
'settings.mcp.clientConfigHintOAuth': 'Replace <your_client_id> and <your_client_secret> with the credentials shown in the OAuth 2.1 client you created above. mcp-remote will open your browser to complete the authorization the first time you connect. The path to npx may need to be adjusted for your system (e.g. C:\PROGRA~1\nodejs\npx.cmd on Windows).',
|
||||
'settings.mcp.clientConfigHintOAuth': 'Замените <your_client_id> и <your_client_secret> на учётные данные из созданного выше клиента OAuth 2.1. При первом подключении mcp-remote откроет браузер для завершения авторизации. Путь к npx может потребовать настройки для вашей системы (например, C:\\PROGRA~1\\nodejs\\npx.cmd в Windows).',
|
||||
'settings.mcp.copy': 'Копировать',
|
||||
'settings.mcp.copied': 'Скопировано!',
|
||||
'settings.mcp.apiTokens': 'API-токены',
|
||||
@@ -250,6 +247,48 @@ const ru: Record<string, string> = {
|
||||
'settings.mcp.toast.createError': 'Не удалось создать токен',
|
||||
'settings.mcp.toast.deleted': 'Токен удалён',
|
||||
'settings.mcp.toast.deleteError': 'Не удалось удалить токен',
|
||||
'settings.mcp.apiTokensDeprecated': 'API-токены устарели и будут удалены в будущей версии. Пожалуйста, используйте клиенты OAuth 2.1.',
|
||||
'settings.oauth.clients': 'Клиенты OAuth 2.1',
|
||||
'settings.oauth.clientsHint': 'Зарегистрируйте клиенты OAuth 2.1, чтобы сторонние MCP-приложения (Claude Web, Cursor и др.) могли подключаться без статических токенов.',
|
||||
'settings.oauth.createClient': 'Новый клиент',
|
||||
'settings.oauth.noClients': 'Нет зарегистрированных клиентов OAuth.',
|
||||
'settings.oauth.clientId': 'ID клиента',
|
||||
'settings.oauth.clientSecret': 'Секрет клиента',
|
||||
'settings.oauth.deleteClient': 'Удалить клиента',
|
||||
'settings.oauth.deleteClientMessage': 'Этот клиент и все активные сессии будут удалены навсегда. Любое приложение, использующее его, немедленно потеряет доступ.',
|
||||
'settings.oauth.rotateSecret': 'Обновить секрет',
|
||||
'settings.oauth.rotateSecretMessage': 'Будет сгенерирован новый секрет клиента, а все существующие сессии будут немедленно аннулированы. Обновите приложение перед закрытием этого диалога.',
|
||||
'settings.oauth.rotateSecretConfirm': 'Обновить',
|
||||
'settings.oauth.rotateSecretConfirming': 'Обновление…',
|
||||
'settings.oauth.rotateSecretDoneTitle': 'Новый секрет сгенерирован',
|
||||
'settings.oauth.rotateSecretDoneWarning': 'Этот секрет отображается только один раз. Скопируйте его сейчас и обновите приложение — все предыдущие сессии были аннулированы.',
|
||||
'settings.oauth.activeSessions': 'Активные сессии OAuth',
|
||||
'settings.oauth.sessionScopes': 'Области доступа',
|
||||
'settings.oauth.sessionExpires': 'Истекает',
|
||||
'settings.oauth.revoke': 'Отозвать',
|
||||
'settings.oauth.revokeSession': 'Отозвать сессию',
|
||||
'settings.oauth.revokeSessionMessage': 'Это немедленно отзовёт доступ для данной сессии OAuth.',
|
||||
'settings.oauth.modal.createTitle': 'Зарегистрировать клиент OAuth',
|
||||
'settings.oauth.modal.presets': 'Быстрые настройки',
|
||||
'settings.oauth.modal.clientName': 'Название приложения',
|
||||
'settings.oauth.modal.clientNamePlaceholder': 'напр. Claude Web, Моё MCP-приложение',
|
||||
'settings.oauth.modal.redirectUris': 'URI перенаправления',
|
||||
'settings.oauth.modal.redirectUrisPlaceholder': 'https://your-app.com/callback\nhttps://your-app.com/auth',
|
||||
'settings.oauth.modal.redirectUrisHint': 'Один URI на строку. Требуется HTTPS (localhost исключён). Требуется точное совпадение.',
|
||||
'settings.oauth.modal.scopes': 'Разрешённые области доступа',
|
||||
'settings.oauth.modal.scopesHint': 'list_trips и get_trip_summary всегда доступны — область не требуется. Они помогают ИИ находить нужные ID поездок.',
|
||||
'settings.oauth.modal.selectAll': 'Выбрать все',
|
||||
'settings.oauth.modal.deselectAll': 'Снять выбор',
|
||||
'settings.oauth.modal.creating': 'Регистрация…',
|
||||
'settings.oauth.modal.create': 'Зарегистрировать клиента',
|
||||
'settings.oauth.modal.createdTitle': 'Клиент зарегистрирован',
|
||||
'settings.oauth.modal.createdWarning': 'Секрет клиента отображается только один раз. Скопируйте его сейчас — его нельзя будет восстановить.',
|
||||
'settings.oauth.toast.createError': 'Не удалось зарегистрировать клиент OAuth',
|
||||
'settings.oauth.toast.deleted': 'Клиент OAuth удалён',
|
||||
'settings.oauth.toast.deleteError': 'Не удалось удалить клиент OAuth',
|
||||
'settings.oauth.toast.revoked': 'Сессия отозвана',
|
||||
'settings.oauth.toast.revokeError': 'Не удалось отозвать сессию',
|
||||
'settings.oauth.toast.rotateError': 'Не удалось обновить секрет клиента',
|
||||
'settings.account': 'Аккаунт',
|
||||
'settings.about': 'О приложении',
|
||||
'settings.about.reportBug': 'Сообщить об ошибке',
|
||||
@@ -1018,6 +1057,7 @@ const ru: Record<string, string> = {
|
||||
'budget.totalBudget': 'Общий бюджет',
|
||||
'budget.byCategory': 'По категориям',
|
||||
'budget.editTooltip': 'Нажмите для редактирования',
|
||||
'budget.linkedToReservation': 'Связано с бронированием — редактируйте название там',
|
||||
'budget.confirm.deleteCategory': 'Вы уверены, что хотите удалить категорию «{name}» с {count} записями?',
|
||||
'budget.deleteCategory': 'Удалить категорию',
|
||||
'budget.perPerson': 'На человека',
|
||||
@@ -1116,6 +1156,9 @@ const ru: Record<string, string> = {
|
||||
'packing.template': 'Шаблон',
|
||||
'packing.templateApplied': '{count} вещей добавлено из шаблона',
|
||||
'packing.templateError': 'Ошибка применения шаблона',
|
||||
'packing.saveAsTemplate': 'Сохранить как шаблон',
|
||||
'packing.templateName': 'Название шаблона',
|
||||
'packing.templateSaved': 'Список вещей сохранён как шаблон',
|
||||
'packing.assignUser': 'Назначить пользователя',
|
||||
'packing.noMembers': 'Нет участников',
|
||||
'packing.bags': 'Багаж',
|
||||
@@ -1401,8 +1444,6 @@ const ru: Record<string, string> = {
|
||||
'memories.reviewTitle': 'Проверьте ваши фото',
|
||||
'memories.reviewHint': 'Нажмите на фото, чтобы исключить его из общего доступа.',
|
||||
'memories.shareCount': 'Поделиться ({count} фото)',
|
||||
'memories.immichUrl': 'URL сервера Immich',
|
||||
'memories.immichApiKey': 'API-ключ',
|
||||
'memories.testConnection': 'Проверить подключение',
|
||||
'memories.testFirst': 'Сначала проверьте подключение',
|
||||
'memories.connected': 'Подключено',
|
||||
@@ -1699,6 +1740,70 @@ const ru: Record<string, string> = {
|
||||
'notif.generic.text': 'У вас новое уведомление',
|
||||
'notif.dev.unknown_event.title': '[DEV] Неизвестное событие',
|
||||
'notif.dev.unknown_event.text': 'Тип события "{event}" не зарегистрирован в EVENT_NOTIFICATION_CONFIG',
|
||||
|
||||
// OAuth scope groups
|
||||
'oauth.scope.group.trips': 'Поездки',
|
||||
'oauth.scope.group.places': 'Места',
|
||||
'oauth.scope.group.atlas': 'Atlas',
|
||||
'oauth.scope.group.packing': 'Вещи',
|
||||
'oauth.scope.group.todos': 'Задачи',
|
||||
'oauth.scope.group.budget': 'Бюджет',
|
||||
'oauth.scope.group.reservations': 'Бронирования',
|
||||
'oauth.scope.group.collab': 'Сотрудничество',
|
||||
'oauth.scope.group.notifications': 'Уведомления',
|
||||
'oauth.scope.group.vacay': 'Отпуск',
|
||||
'oauth.scope.group.geo': 'Geo',
|
||||
'oauth.scope.group.weather': 'Погода',
|
||||
|
||||
// OAuth scope labels & descriptions
|
||||
'oauth.scope.trips:read.label': 'Просмотр поездок и маршрутов',
|
||||
'oauth.scope.trips:read.description': 'Чтение поездок, дней, заметок и участников',
|
||||
'oauth.scope.trips:write.label': 'Редактирование поездок и маршрутов',
|
||||
'oauth.scope.trips:write.description': 'Создание и обновление поездок, дней, заметок и управление участниками',
|
||||
'oauth.scope.trips:delete.label': 'Удаление поездок',
|
||||
'oauth.scope.trips:delete.description': 'Безвозвратное удаление поездок — это действие необратимо',
|
||||
'oauth.scope.trips:share.label': 'Управление ссылками на совместный доступ',
|
||||
'oauth.scope.trips:share.description': 'Создание, обновление и отзыв публичных ссылок на поездки',
|
||||
'oauth.scope.places:read.label': 'Просмотр мест и данных карты',
|
||||
'oauth.scope.places:read.description': 'Чтение мест, назначений по дням, тегов и категорий',
|
||||
'oauth.scope.places:write.label': 'Управление местами',
|
||||
'oauth.scope.places:write.description': 'Создание, обновление и удаление мест, назначений и тегов',
|
||||
'oauth.scope.atlas:read.label': 'Просмотр Atlas',
|
||||
'oauth.scope.atlas:read.description': 'Чтение посещённых стран, регионов и списка желаний',
|
||||
'oauth.scope.atlas:write.label': 'Управление Atlas',
|
||||
'oauth.scope.atlas:write.description': 'Отмечать посещённые страны и регионы, управлять списком желаний',
|
||||
'oauth.scope.packing:read.label': 'Просмотр списков вещей',
|
||||
'oauth.scope.packing:read.description': 'Чтение вещей, сумок и назначений категорий',
|
||||
'oauth.scope.packing:write.label': 'Управление списками вещей',
|
||||
'oauth.scope.packing:write.description': 'Добавление, обновление, удаление, отметка и переупорядочивание вещей и сумок',
|
||||
'oauth.scope.todos:read.label': 'Просмотр списков задач',
|
||||
'oauth.scope.todos:read.description': 'Чтение задач поездки и назначений категорий',
|
||||
'oauth.scope.todos:write.label': 'Управление списками задач',
|
||||
'oauth.scope.todos:write.description': 'Создание, обновление, отметка, удаление и переупорядочивание задач',
|
||||
'oauth.scope.budget:read.label': 'Просмотр бюджета',
|
||||
'oauth.scope.budget:read.description': 'Чтение статей бюджета и разбивки расходов',
|
||||
'oauth.scope.budget:write.label': 'Управление бюджетом',
|
||||
'oauth.scope.budget:write.description': 'Создание, обновление и удаление статей бюджета',
|
||||
'oauth.scope.reservations:read.label': 'Просмотр бронирований',
|
||||
'oauth.scope.reservations:read.description': 'Чтение бронирований и сведений о проживании',
|
||||
'oauth.scope.reservations:write.label': 'Управление бронированиями',
|
||||
'oauth.scope.reservations:write.description': 'Создание, обновление, удаление и переупорядочивание бронирований',
|
||||
'oauth.scope.collab:read.label': 'Просмотр совместной работы',
|
||||
'oauth.scope.collab:read.description': 'Чтение совместных заметок, опросов и сообщений',
|
||||
'oauth.scope.collab:write.label': 'Управление совместной работой',
|
||||
'oauth.scope.collab:write.description': 'Создание, обновление и удаление заметок, опросов и сообщений',
|
||||
'oauth.scope.notifications:read.label': 'Просмотр уведомлений',
|
||||
'oauth.scope.notifications:read.description': 'Чтение уведомлений в приложении и количества непрочитанных',
|
||||
'oauth.scope.notifications:write.label': 'Управление уведомлениями',
|
||||
'oauth.scope.notifications:write.description': 'Отмечать уведомления как прочитанные и отвечать на них',
|
||||
'oauth.scope.vacay:read.label': 'Просмотр планов отпуска',
|
||||
'oauth.scope.vacay:read.description': 'Чтение данных планирования отпуска, записей и статистики',
|
||||
'oauth.scope.vacay:write.label': 'Управление планами отпуска',
|
||||
'oauth.scope.vacay:write.description': 'Создание и управление записями отпуска, праздниками и командными планами',
|
||||
'oauth.scope.geo:read.label': 'Карты и геокодирование',
|
||||
'oauth.scope.geo:read.description': 'Поиск мест, разрешение URL карт и обратное геокодирование координат',
|
||||
'oauth.scope.weather:read.label': 'Прогнозы погоды',
|
||||
'oauth.scope.weather:read.description': 'Получение прогнозов погоды для мест и дат поездки',
|
||||
}
|
||||
|
||||
export default ru
|
||||
|
||||
@@ -179,9 +179,6 @@ const zh: Record<string, string> = {
|
||||
'admin.notifications.none': '已禁用',
|
||||
'admin.notifications.email': '电子邮件 (SMTP)',
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': '通知事件',
|
||||
'admin.notifications.eventsHint': '选择哪些事件为所有用户触发通知。',
|
||||
'admin.notifications.configureFirst': '请先在下方配置 SMTP 或 Webhook,然后启用事件。',
|
||||
'admin.notifications.save': '保存通知设置',
|
||||
'admin.notifications.saved': '通知设置已保存',
|
||||
'admin.notifications.testWebhook': '发送测试 Webhook',
|
||||
@@ -228,7 +225,7 @@ const zh: Record<string, string> = {
|
||||
'settings.mcp.endpoint': 'MCP 端点',
|
||||
'settings.mcp.clientConfig': '客户端配置',
|
||||
'settings.mcp.clientConfigHint': '将 <your_token> 替换为下方列表中的 API 令牌。npx 的路径可能需要根据您的系统进行调整(例如 Windows 上为 C:\\PROGRA~1\\nodejs\\npx.cmd)。',
|
||||
'settings.mcp.clientConfigHintOAuth': 'Replace <your_client_id> and <your_client_secret> with the credentials shown in the OAuth 2.1 client you created above. mcp-remote will open your browser to complete the authorization the first time you connect. The path to npx may need to be adjusted for your system (e.g. C:\PROGRA~1\nodejs\npx.cmd on Windows).',
|
||||
'settings.mcp.clientConfigHintOAuth': '将 <your_client_id> 和 <your_client_secret> 替换为上方创建的 OAuth 2.1 客户端凭据。首次连接时,mcp-remote 将打开浏览器完成授权。npx 的路径可能需要根据您的系统进行调整(例如 Windows 上为 C:\\PROGRA~1\\nodejs\\npx.cmd)。',
|
||||
'settings.mcp.copy': '复制',
|
||||
'settings.mcp.copied': '已复制!',
|
||||
'settings.mcp.apiTokens': 'API 令牌',
|
||||
@@ -250,6 +247,48 @@ const zh: Record<string, string> = {
|
||||
'settings.mcp.toast.createError': '创建令牌失败',
|
||||
'settings.mcp.toast.deleted': '令牌已删除',
|
||||
'settings.mcp.toast.deleteError': '删除令牌失败',
|
||||
'settings.mcp.apiTokensDeprecated': 'API 令牌已弃用,将在未来版本中移除。请改用 OAuth 2.1 客户端。',
|
||||
'settings.oauth.clients': 'OAuth 2.1 客户端',
|
||||
'settings.oauth.clientsHint': '注册 OAuth 2.1 客户端,让第三方 MCP 应用程序(Claude Web、Cursor 等)无需静态令牌即可连接。',
|
||||
'settings.oauth.createClient': '新建客户端',
|
||||
'settings.oauth.noClients': '没有已注册的 OAuth 客户端。',
|
||||
'settings.oauth.clientId': '客户端 ID',
|
||||
'settings.oauth.clientSecret': '客户端密钥',
|
||||
'settings.oauth.deleteClient': '删除客户端',
|
||||
'settings.oauth.deleteClientMessage': '此客户端及所有活跃会话将被永久删除。使用此客户端的任何应用程序将立即失去访问权限。',
|
||||
'settings.oauth.rotateSecret': '轮换密钥',
|
||||
'settings.oauth.rotateSecretMessage': '将生成新的客户端密钥,所有现有会话将立即失效。在关闭此对话框之前,请更新您的应用程序。',
|
||||
'settings.oauth.rotateSecretConfirm': '轮换',
|
||||
'settings.oauth.rotateSecretConfirming': '轮换中…',
|
||||
'settings.oauth.rotateSecretDoneTitle': '已生成新密钥',
|
||||
'settings.oauth.rotateSecretDoneWarning': '此密钥仅显示一次。请立即复制并更新您的应用程序——所有之前的会话已失效。',
|
||||
'settings.oauth.activeSessions': '活跃的 OAuth 会话',
|
||||
'settings.oauth.sessionScopes': '权限范围',
|
||||
'settings.oauth.sessionExpires': '过期时间',
|
||||
'settings.oauth.revoke': '撤销',
|
||||
'settings.oauth.revokeSession': '撤销会话',
|
||||
'settings.oauth.revokeSessionMessage': '这将立即撤销此 OAuth 会话的访问权限。',
|
||||
'settings.oauth.modal.createTitle': '注册 OAuth 客户端',
|
||||
'settings.oauth.modal.presets': '快速预设',
|
||||
'settings.oauth.modal.clientName': '应用程序名称',
|
||||
'settings.oauth.modal.clientNamePlaceholder': '例如 Claude Web、我的 MCP 应用',
|
||||
'settings.oauth.modal.redirectUris': '重定向 URI',
|
||||
'settings.oauth.modal.redirectUrisPlaceholder': 'https://your-app.com/callback\nhttps://your-app.com/auth',
|
||||
'settings.oauth.modal.redirectUrisHint': '每行一个 URI。需要 HTTPS(localhost 除外)。要求精确匹配。',
|
||||
'settings.oauth.modal.scopes': '允许的权限范围',
|
||||
'settings.oauth.modal.scopesHint': 'list_trips 和 get_trip_summary 始终可用——无需权限范围。它们帮助 AI 发现所需的行程 ID。',
|
||||
'settings.oauth.modal.selectAll': '全选',
|
||||
'settings.oauth.modal.deselectAll': '取消全选',
|
||||
'settings.oauth.modal.creating': '注册中…',
|
||||
'settings.oauth.modal.create': '注册客户端',
|
||||
'settings.oauth.modal.createdTitle': '客户端已注册',
|
||||
'settings.oauth.modal.createdWarning': '客户端密钥仅显示一次。请立即复制——无法恢复。',
|
||||
'settings.oauth.toast.createError': '注册 OAuth 客户端失败',
|
||||
'settings.oauth.toast.deleted': 'OAuth 客户端已删除',
|
||||
'settings.oauth.toast.deleteError': '删除 OAuth 客户端失败',
|
||||
'settings.oauth.toast.revoked': '会话已撤销',
|
||||
'settings.oauth.toast.revokeError': '撤销会话失败',
|
||||
'settings.oauth.toast.rotateError': '轮换客户端密钥失败',
|
||||
'settings.account': '账户',
|
||||
'settings.about': '关于',
|
||||
'settings.about.reportBug': '报告错误',
|
||||
@@ -1018,6 +1057,7 @@ const zh: Record<string, string> = {
|
||||
'budget.totalBudget': '总预算',
|
||||
'budget.byCategory': '按分类',
|
||||
'budget.editTooltip': '点击编辑',
|
||||
'budget.linkedToReservation': '已关联到预订——请在那里编辑名称',
|
||||
'budget.confirm.deleteCategory': '确定删除分类「{name}」及其 {count} 个条目?',
|
||||
'budget.deleteCategory': '删除分类',
|
||||
'budget.perPerson': '人均',
|
||||
@@ -1116,6 +1156,9 @@ const zh: Record<string, string> = {
|
||||
'packing.template': '模板',
|
||||
'packing.templateApplied': '已从模板添加 {count} 个物品',
|
||||
'packing.templateError': '应用模板失败',
|
||||
'packing.saveAsTemplate': '保存为模板',
|
||||
'packing.templateName': '模板名称',
|
||||
'packing.templateSaved': '行李清单已保存为模板',
|
||||
'packing.assignUser': '分配用户',
|
||||
'packing.noMembers': '无成员',
|
||||
'packing.bags': '行李',
|
||||
@@ -1401,8 +1444,6 @@ const zh: Record<string, string> = {
|
||||
'memories.reviewTitle': '审查您的照片',
|
||||
'memories.reviewHint': '点击照片以将其从分享中排除。',
|
||||
'memories.shareCount': '分享 {count} 张照片',
|
||||
'memories.immichUrl': 'Immich 服务器地址',
|
||||
'memories.immichApiKey': 'API 密钥',
|
||||
'memories.testConnection': '测试连接',
|
||||
'memories.testFirst': '请先测试连接',
|
||||
'memories.connected': '已连接',
|
||||
@@ -1699,6 +1740,70 @@ const zh: Record<string, string> = {
|
||||
'notif.generic.text': '您有一条新通知',
|
||||
'notif.dev.unknown_event.title': '[DEV] 未知事件',
|
||||
'notif.dev.unknown_event.text': '事件类型 "{event}" 未在 EVENT_NOTIFICATION_CONFIG 中注册',
|
||||
|
||||
// OAuth scope groups
|
||||
'oauth.scope.group.trips': '行程',
|
||||
'oauth.scope.group.places': '地点',
|
||||
'oauth.scope.group.atlas': 'Atlas',
|
||||
'oauth.scope.group.packing': '行李',
|
||||
'oauth.scope.group.todos': '待办事项',
|
||||
'oauth.scope.group.budget': '预算',
|
||||
'oauth.scope.group.reservations': '预订',
|
||||
'oauth.scope.group.collab': '协作',
|
||||
'oauth.scope.group.notifications': '通知',
|
||||
'oauth.scope.group.vacay': '假期',
|
||||
'oauth.scope.group.geo': 'Geo',
|
||||
'oauth.scope.group.weather': '天气',
|
||||
|
||||
// OAuth scope labels & descriptions
|
||||
'oauth.scope.trips:read.label': '查看行程和行程计划',
|
||||
'oauth.scope.trips:read.description': '读取行程、天数、每日笔记和成员',
|
||||
'oauth.scope.trips:write.label': '编辑行程和行程计划',
|
||||
'oauth.scope.trips:write.description': '创建和更新行程、天数、笔记并管理成员',
|
||||
'oauth.scope.trips:delete.label': '删除行程',
|
||||
'oauth.scope.trips:delete.description': '永久删除整个行程——此操作不可撤销',
|
||||
'oauth.scope.trips:share.label': '管理分享链接',
|
||||
'oauth.scope.trips:share.description': '创建、更新和撤销行程的公开分享链接',
|
||||
'oauth.scope.places:read.label': '查看地点和地图数据',
|
||||
'oauth.scope.places:read.description': '读取地点、每日分配、标签和分类',
|
||||
'oauth.scope.places:write.label': '管理地点',
|
||||
'oauth.scope.places:write.description': '创建、更新和删除地点、分配和标签',
|
||||
'oauth.scope.atlas:read.label': '查看 Atlas',
|
||||
'oauth.scope.atlas:read.description': '读取已访问国家、地区和心愿清单',
|
||||
'oauth.scope.atlas:write.label': '管理 Atlas',
|
||||
'oauth.scope.atlas:write.description': '标记已访问国家和地区,管理心愿清单',
|
||||
'oauth.scope.packing:read.label': '查看行李清单',
|
||||
'oauth.scope.packing:read.description': '读取行李物品、包袋和分类负责人',
|
||||
'oauth.scope.packing:write.label': '管理行李清单',
|
||||
'oauth.scope.packing:write.description': '添加、更新、删除、勾选和重新排列行李物品和包袋',
|
||||
'oauth.scope.todos:read.label': '查看待办清单',
|
||||
'oauth.scope.todos:read.description': '读取行程待办事项和分类负责人',
|
||||
'oauth.scope.todos:write.label': '管理待办清单',
|
||||
'oauth.scope.todos:write.description': '创建、更新、勾选、删除和重新排列待办事项',
|
||||
'oauth.scope.budget:read.label': '查看预算',
|
||||
'oauth.scope.budget:read.description': '读取预算条目和费用明细',
|
||||
'oauth.scope.budget:write.label': '管理预算',
|
||||
'oauth.scope.budget:write.description': '创建、更新和删除预算条目',
|
||||
'oauth.scope.reservations:read.label': '查看预订',
|
||||
'oauth.scope.reservations:read.description': '读取预订和住宿详情',
|
||||
'oauth.scope.reservations:write.label': '管理预订',
|
||||
'oauth.scope.reservations:write.description': '创建、更新、删除和重新排列预订',
|
||||
'oauth.scope.collab:read.label': '查看协作',
|
||||
'oauth.scope.collab:read.description': '读取协作笔记、投票和消息',
|
||||
'oauth.scope.collab:write.label': '管理协作',
|
||||
'oauth.scope.collab:write.description': '创建、更新和删除协作笔记、投票和消息',
|
||||
'oauth.scope.notifications:read.label': '查看通知',
|
||||
'oauth.scope.notifications:read.description': '读取应用内通知和未读数量',
|
||||
'oauth.scope.notifications:write.label': '管理通知',
|
||||
'oauth.scope.notifications:write.description': '将通知标记为已读并回复',
|
||||
'oauth.scope.vacay:read.label': '查看假期计划',
|
||||
'oauth.scope.vacay:read.description': '读取假期计划数据、条目和统计',
|
||||
'oauth.scope.vacay:write.label': '管理假期计划',
|
||||
'oauth.scope.vacay:write.description': '创建和管理假期条目、节假日和团队计划',
|
||||
'oauth.scope.geo:read.label': '地图和地理编码',
|
||||
'oauth.scope.geo:read.description': '搜索位置、解析地图 URL 和反向地理编码坐标',
|
||||
'oauth.scope.weather:read.label': '天气预报',
|
||||
'oauth.scope.weather:read.description': '获取行程地点和日期的天气预报',
|
||||
}
|
||||
|
||||
export default zh
|
||||
|
||||
@@ -113,6 +113,8 @@ const zhTw: Record<string, string> = {
|
||||
'dashboard.tripDescriptionPlaceholder': '這次旅行是關於什麼的?',
|
||||
'dashboard.startDate': '開始日期',
|
||||
'dashboard.endDate': '結束日期',
|
||||
'dashboard.dayCount': '天數',
|
||||
'dashboard.dayCountHint': '未設定旅行日期時,要規劃的天數。',
|
||||
'dashboard.noDateHint': '未設定日期——將預設建立 7 天。你可以隨時修改。',
|
||||
'dashboard.coverImage': '封面圖片',
|
||||
'dashboard.addCoverImage': '新增封面圖片',
|
||||
@@ -127,6 +129,12 @@ const zhTw: Record<string, string> = {
|
||||
// Settings
|
||||
'settings.title': '設定',
|
||||
'settings.subtitle': '配置你的個人設定',
|
||||
'settings.tabs.display': '顯示',
|
||||
'settings.tabs.map': '地圖',
|
||||
'settings.tabs.notifications': '通知',
|
||||
'settings.tabs.integrations': '整合',
|
||||
'settings.tabs.account': '帳戶',
|
||||
'settings.tabs.about': '關於',
|
||||
'settings.map': '地圖',
|
||||
'settings.mapTemplate': '地圖模板',
|
||||
'settings.mapTemplatePlaceholder.select': '選擇模板...',
|
||||
@@ -163,6 +171,19 @@ const zhTw: Record<string, string> = {
|
||||
'settings.notifyCollabMessage': '聊天訊息 (Collab)',
|
||||
'settings.notifyPackingTagged': '行李清單:分配',
|
||||
'settings.notifyWebhook': 'Webhook 通知',
|
||||
'settings.notifyVersionAvailable': '有新版本可用',
|
||||
'settings.notificationPreferences.email': '電子郵件',
|
||||
'settings.notificationPreferences.webhook': 'Webhook',
|
||||
'settings.notificationPreferences.inapp': '應用程式內',
|
||||
'settings.notificationPreferences.noChannels': '未配置通知渠道。請聯絡管理員設定電子郵件或 Webhook 通知。',
|
||||
'settings.webhookUrl.label': 'Webhook URL',
|
||||
'settings.webhookUrl.placeholder': 'https://discord.com/api/webhooks/...',
|
||||
'settings.webhookUrl.hint': '輸入您的 Discord、Slack 或自訂 Webhook URL 以接收通知。',
|
||||
'settings.webhookUrl.save': '儲存',
|
||||
'settings.webhookUrl.saved': 'Webhook URL 已儲存',
|
||||
'settings.webhookUrl.test': '測試',
|
||||
'settings.webhookUrl.testSuccess': '測試 Webhook 傳送成功',
|
||||
'settings.webhookUrl.testFailed': '測試 Webhook 傳送失敗',
|
||||
'settings.notificationsDisabled': '通知尚未配置。請聯絡管理員啟用電子郵件或 Webhook 通知。',
|
||||
'settings.notificationsActive': '活躍頻道',
|
||||
'settings.notificationsManagedByAdmin': '通知事件由管理員配置。',
|
||||
@@ -171,18 +192,26 @@ const zhTw: Record<string, string> = {
|
||||
'admin.notifications.none': '已停用',
|
||||
'admin.notifications.email': '電子郵件 (SMTP)',
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': '通知事件',
|
||||
'admin.notifications.eventsHint': '選擇哪些事件為所有使用者觸發通知。',
|
||||
'admin.notifications.configureFirst': '請先在下方配置 SMTP 或 Webhook,然後啟用事件。',
|
||||
'admin.notifications.save': '儲存通知設定',
|
||||
'admin.notifications.saved': '通知設定已儲存',
|
||||
'admin.notifications.testWebhook': '傳送測試 Webhook',
|
||||
'admin.notifications.testWebhookSuccess': '測試 Webhook 傳送成功',
|
||||
'admin.notifications.testWebhookFailed': '測試 Webhook 傳送失敗',
|
||||
'admin.notifications.emailPanel.title': '電子郵件 (SMTP)',
|
||||
'admin.notifications.webhookPanel.title': 'Webhook',
|
||||
'admin.notifications.inappPanel.title': '應用程式內通知',
|
||||
'admin.notifications.inappPanel.hint': '應用程式內通知始終啟用,無法全域性停用。',
|
||||
'admin.notifications.adminWebhookPanel.title': '管理員 Webhook',
|
||||
'admin.notifications.adminWebhookPanel.hint': '此 Webhook 專用於管理員通知(例如版本提醒)。它與每位使用者的 Webhook 分開,設定後始終會觸發。',
|
||||
'admin.notifications.adminWebhookPanel.saved': '管理員 Webhook URL 已儲存',
|
||||
'admin.notifications.adminWebhookPanel.testSuccess': '測試 Webhook 傳送成功',
|
||||
'admin.notifications.adminWebhookPanel.testFailed': '測試 Webhook 傳送失敗',
|
||||
'admin.notifications.adminWebhookPanel.alwaysOnHint': '配置 URL 後,管理員 Webhook 始終觸發',
|
||||
'admin.notifications.adminNotificationsHint': '配置哪些渠道傳遞僅管理員通知(例如版本提醒)。',
|
||||
'admin.smtp.title': '郵件與通知',
|
||||
'admin.smtp.hint': '用於傳送電子郵件通知的 SMTP 配置。',
|
||||
'admin.smtp.testButton': '傳送測試郵件',
|
||||
'admin.webhook.hint': '向外部 Webhook 傳送通知(Discord、Slack 等)。',
|
||||
'admin.webhook.hint': '允許使用者配置自己的 Webhook URL 以接收通知(Discord、Slack 等)。',
|
||||
'admin.smtp.testSuccess': '測試郵件傳送成功',
|
||||
'admin.smtp.testFailed': '測試郵件傳送失敗',
|
||||
'dayplan.icsTooltip': '匯出日曆 (ICS)',
|
||||
@@ -220,7 +249,7 @@ const zhTw: Record<string, string> = {
|
||||
'settings.mcp.endpoint': 'MCP 端點',
|
||||
'settings.mcp.clientConfig': '客戶端配置',
|
||||
'settings.mcp.clientConfigHint': '將 <your_token> 替換為下方列表中的 API 令牌。npx 的路徑可能需要根據您的系統進行調整(例如 Windows 上為 C:\\PROGRA~1\\nodejs\\npx.cmd)。',
|
||||
'settings.mcp.clientConfigHintOAuth': 'Replace <your_client_id> and <your_client_secret> with the credentials shown in the OAuth 2.1 client you created above. mcp-remote will open your browser to complete the authorization the first time you connect. The path to npx may need to be adjusted for your system (e.g. C:\PROGRA~1\nodejs\npx.cmd on Windows).',
|
||||
'settings.mcp.clientConfigHintOAuth': '將 <your_client_id> 和 <your_client_secret> 替換為上方建立的 OAuth 2.1 客戶端所顯示的憑據。首次連線時,mcp-remote 將開啟瀏覽器完成授權。npx 的路徑可能需要根據您的系統進行調整(例如 Windows 上為 C:\\PROGRA~1\\nodejs\\npx.cmd)。',
|
||||
'settings.mcp.copy': '複製',
|
||||
'settings.mcp.copied': '已複製!',
|
||||
'settings.mcp.apiTokens': 'API 令牌',
|
||||
@@ -242,8 +271,58 @@ const zhTw: Record<string, string> = {
|
||||
'settings.mcp.toast.createError': '建立令牌失敗',
|
||||
'settings.mcp.toast.deleted': '令牌已刪除',
|
||||
'settings.mcp.toast.deleteError': '刪除令牌失敗',
|
||||
'settings.mcp.apiTokensDeprecated': 'API 金鑰已棄用,將於未來版本中移除。請改用 OAuth 2.1 客戶端。',
|
||||
'settings.oauth.clients': 'OAuth 2.1 客戶端',
|
||||
'settings.oauth.clientsHint': '註冊 OAuth 2.1 客戶端,讓第三方 MCP 應用程式(Claude Web、Cursor 等)無需靜態金鑰即可連線。',
|
||||
'settings.oauth.createClient': '新增客戶端',
|
||||
'settings.oauth.noClients': '尚無已註冊的 OAuth 客戶端。',
|
||||
'settings.oauth.clientId': '客戶端 ID',
|
||||
'settings.oauth.clientSecret': '客戶端密鑰',
|
||||
'settings.oauth.deleteClient': '刪除客戶端',
|
||||
'settings.oauth.deleteClientMessage': '此客戶端及所有活躍工作階段將被永久刪除。任何使用此客戶端的應用程式將立即失去存取權限。',
|
||||
'settings.oauth.rotateSecret': '輪換密鑰',
|
||||
'settings.oauth.rotateSecretMessage': '將產生新的客戶端密鑰,所有現有工作階段將立即失效。請在關閉此對話框前更新您的應用程式。',
|
||||
'settings.oauth.rotateSecretConfirm': '輪換',
|
||||
'settings.oauth.rotateSecretConfirming': '輪換中…',
|
||||
'settings.oauth.rotateSecretDoneTitle': '已產生新密鑰',
|
||||
'settings.oauth.rotateSecretDoneWarning': '此密鑰僅顯示一次。請立即複製並更新您的應用程式——所有先前的工作階段已失效。',
|
||||
'settings.oauth.activeSessions': '活躍的 OAuth 工作階段',
|
||||
'settings.oauth.sessionScopes': '授權範圍',
|
||||
'settings.oauth.sessionExpires': '到期時間',
|
||||
'settings.oauth.revoke': '撤銷',
|
||||
'settings.oauth.revokeSession': '撤銷工作階段',
|
||||
'settings.oauth.revokeSessionMessage': '這將立即撤銷此 OAuth 工作階段的存取權限。',
|
||||
'settings.oauth.modal.createTitle': '註冊 OAuth 客戶端',
|
||||
'settings.oauth.modal.presets': '快速預設',
|
||||
'settings.oauth.modal.clientName': '應用程式名稱',
|
||||
'settings.oauth.modal.clientNamePlaceholder': '例如 Claude Web、我的 MCP 應用程式',
|
||||
'settings.oauth.modal.redirectUris': '重新導向 URI',
|
||||
'settings.oauth.modal.redirectUrisPlaceholder': 'https://your-app.com/callback\nhttps://your-app.com/auth',
|
||||
'settings.oauth.modal.redirectUrisHint': '每行一個 URI。需要 HTTPS(localhost 除外)。需要完全符合。',
|
||||
'settings.oauth.modal.scopes': '允許的授權範圍',
|
||||
'settings.oauth.modal.scopesHint': 'list_trips 和 get_trip_summary 始終可用——不需要授權範圍。它們可幫助 AI 找到所需的行程 ID。',
|
||||
'settings.oauth.modal.selectAll': '全選',
|
||||
'settings.oauth.modal.deselectAll': '取消全選',
|
||||
'settings.oauth.modal.creating': '註冊中…',
|
||||
'settings.oauth.modal.create': '註冊客戶端',
|
||||
'settings.oauth.modal.createdTitle': '客戶端已註冊',
|
||||
'settings.oauth.modal.createdWarning': '客戶端密鑰僅顯示一次。請立即複製——無法恢復。',
|
||||
'settings.oauth.toast.createError': '註冊 OAuth 客戶端失敗',
|
||||
'settings.oauth.toast.deleted': 'OAuth 客戶端已刪除',
|
||||
'settings.oauth.toast.deleteError': '刪除 OAuth 客戶端失敗',
|
||||
'settings.oauth.toast.revoked': '工作階段已撤銷',
|
||||
'settings.oauth.toast.revokeError': '撤銷工作階段失敗',
|
||||
'settings.oauth.toast.rotateError': '輪換客戶端密鑰失敗',
|
||||
'settings.account': '賬戶',
|
||||
'settings.about': '關於',
|
||||
'settings.about.reportBug': '回報錯誤',
|
||||
'settings.about.reportBugHint': '發現問題?告訴我們',
|
||||
'settings.about.featureRequest': '功能建議',
|
||||
'settings.about.featureRequestHint': '建議新功能',
|
||||
'settings.about.wikiHint': '文件與指南',
|
||||
'settings.about.description': 'TREK 是一款自架旅遊規劃器,幫助您從最初構想到最後回憶,整理每次旅行。日程規劃、預算、行李清單、照片及更多功能——全部集中在您自己的伺服器上。',
|
||||
'settings.about.madeWith': '以',
|
||||
'settings.about.madeBy': '由 Maurice 及不斷成長的開源社群製作。',
|
||||
'settings.username': '使用者名稱',
|
||||
'settings.email': '郵箱',
|
||||
'settings.role': '角色',
|
||||
@@ -386,6 +465,7 @@ const zhTw: Record<string, string> = {
|
||||
'admin.tabs.categories': '分類',
|
||||
'admin.tabs.backup': '備份',
|
||||
'admin.tabs.audit': '審計日誌',
|
||||
'admin.tabs.notifications': '通知',
|
||||
'admin.stats.users': '使用者',
|
||||
'admin.stats.trips': '旅行',
|
||||
'admin.stats.places': '地點',
|
||||
@@ -737,8 +817,10 @@ const zhTw: Record<string, string> = {
|
||||
'atlas.unmark': '移除',
|
||||
'atlas.confirmMark': '將此國家標記為已訪問?',
|
||||
'atlas.confirmUnmark': '從已訪問列表中移除此國家?',
|
||||
'atlas.confirmUnmarkRegion': '從已訪問列表中移除此地區?',
|
||||
'atlas.markVisited': '標記為已訪問',
|
||||
'atlas.markVisitedHint': '將此國家新增到已訪問列表',
|
||||
'atlas.markRegionVisitedHint': '將此地區新增到已訪問列表',
|
||||
'atlas.addToBucket': '新增到心願單',
|
||||
'atlas.addPoi': '新增地點',
|
||||
'atlas.searchCountry': '搜尋國家...',
|
||||
@@ -752,6 +834,8 @@ const zhTw: Record<string, string> = {
|
||||
'trip.tabs.reservationsShort': '預訂',
|
||||
'trip.tabs.packing': '行李清單',
|
||||
'trip.tabs.packingShort': '行李',
|
||||
'trip.tabs.lists': '清單',
|
||||
'trip.tabs.listsShort': '清單',
|
||||
'trip.tabs.budget': '預算',
|
||||
'trip.tabs.files': '檔案',
|
||||
'trip.loading': '載入旅行中...',
|
||||
@@ -946,6 +1030,32 @@ const zhTw: Record<string, string> = {
|
||||
'reservations.linkAssignment': '關聯日程分配',
|
||||
'reservations.pickAssignment': '從計劃中選擇一個分配...',
|
||||
'reservations.noAssignment': '無關聯(獨立)',
|
||||
'reservations.price': '價格',
|
||||
'reservations.budgetCategory': '預算分類',
|
||||
'reservations.budgetCategoryPlaceholder': '如:交通、住宿',
|
||||
'reservations.budgetCategoryAuto': '自動(依預訂類型)',
|
||||
'reservations.budgetHint': '儲存時將自動建立預算條目。',
|
||||
'reservations.departureDate': '出發日期',
|
||||
'reservations.arrivalDate': '到達日期',
|
||||
'reservations.departureTime': '出發時間',
|
||||
'reservations.arrivalTime': '到達時間',
|
||||
'reservations.pickupDate': '取車日期',
|
||||
'reservations.returnDate': '還車日期',
|
||||
'reservations.pickupTime': '取車時間',
|
||||
'reservations.returnTime': '還車時間',
|
||||
'reservations.endDate': '結束日期',
|
||||
'reservations.meta.departureTimezone': '出發時區',
|
||||
'reservations.meta.arrivalTimezone': '到達時區',
|
||||
'reservations.span.departure': '出發',
|
||||
'reservations.span.arrival': '到達',
|
||||
'reservations.span.inTransit': '途中',
|
||||
'reservations.span.pickup': '取車',
|
||||
'reservations.span.return': '還車',
|
||||
'reservations.span.active': '進行中',
|
||||
'reservations.span.start': '開始',
|
||||
'reservations.span.end': '結束',
|
||||
'reservations.span.ongoing': '進行中',
|
||||
'reservations.validation.endBeforeStart': '結束日期/時間必須晚於開始日期/時間',
|
||||
|
||||
// Budget
|
||||
'budget.title': '預算',
|
||||
@@ -972,6 +1082,7 @@ const zhTw: Record<string, string> = {
|
||||
'budget.totalBudget': '總預算',
|
||||
'budget.byCategory': '按分類',
|
||||
'budget.editTooltip': '點選編輯',
|
||||
'budget.linkedToReservation': '已連結至預訂——請在那裡編輯名稱',
|
||||
'budget.confirm.deleteCategory': '確定刪除分類「{name}」及其 {count} 個條目?',
|
||||
'budget.deleteCategory': '刪除分類',
|
||||
'budget.perPerson': '人均',
|
||||
@@ -1062,6 +1173,7 @@ const zhTw: Record<string, string> = {
|
||||
'packing.menuCheckAll': '全部勾選',
|
||||
'packing.menuUncheckAll': '取消全部勾選',
|
||||
'packing.menuDeleteCat': '刪除分類',
|
||||
'packing.assignUser': '指派使用者',
|
||||
'packing.addItem': '新增物品',
|
||||
'packing.addItemPlaceholder': '物品名稱...',
|
||||
'packing.addCategory': '新增分類',
|
||||
@@ -1070,7 +1182,9 @@ const zhTw: Record<string, string> = {
|
||||
'packing.template': '模板',
|
||||
'packing.templateApplied': '已從模板新增 {count} 個物品',
|
||||
'packing.templateError': '應用模板失敗',
|
||||
'packing.assignUser': '分配使用者',
|
||||
'packing.saveAsTemplate': '儲存為範本',
|
||||
'packing.templateName': '範本名稱',
|
||||
'packing.templateSaved': '行李清單已儲存為範本',
|
||||
'packing.noMembers': '無成員',
|
||||
'packing.bags': '行李',
|
||||
'packing.noBag': '未分配',
|
||||
@@ -1343,11 +1457,12 @@ const zhTw: Record<string, string> = {
|
||||
|
||||
// Memories / Immich
|
||||
'memories.title': '照片',
|
||||
'memories.notConnected': 'Immich 未連線',
|
||||
'memories.notConnectedHint': '在設定中連線您的 Immich 例項以在此檢視旅行照片。',
|
||||
'memories.notConnected': '{provider_name} 未連線',
|
||||
'memories.notConnectedHint': '在設定中連線您的 {provider_name} 例項以在此旅行中新增照片。',
|
||||
'memories.notConnectedMultipleHint': '在設定中連線以下任一照片提供商:{provider_names} 以在此旅行中新增照片。',
|
||||
'memories.noDates': '為旅行新增日期以載入照片。',
|
||||
'memories.noPhotos': '未找到照片',
|
||||
'memories.noPhotosHint': 'Immich 中未找到此旅行日期範圍內的照片。',
|
||||
'memories.noPhotosHint': '{provider_name} 中未找到此旅行日期範圍內的照片。',
|
||||
'memories.photosFound': '張照片',
|
||||
'memories.fromOthers': '來自他人',
|
||||
'memories.sharePhotos': '分享照片',
|
||||
@@ -1355,26 +1470,31 @@ const zhTw: Record<string, string> = {
|
||||
'memories.reviewTitle': '審查您的照片',
|
||||
'memories.reviewHint': '點選照片以將其從分享中排除。',
|
||||
'memories.shareCount': '分享 {count} 張照片',
|
||||
'memories.immichUrl': 'Immich 伺服器地址',
|
||||
'memories.immichApiKey': 'API 金鑰',
|
||||
'memories.providerUrl': '伺服器 URL',
|
||||
'memories.providerApiKey': 'API 金鑰',
|
||||
'memories.providerUsername': '使用者名稱',
|
||||
'memories.providerPassword': '密碼',
|
||||
'memories.testConnection': '測試連線',
|
||||
'memories.testFirst': '請先測試連線',
|
||||
'memories.connected': '已連線',
|
||||
'memories.disconnected': '未連線',
|
||||
'memories.connectionSuccess': '已連線到 Immich',
|
||||
'memories.connectionError': '無法連線到 Immich',
|
||||
'memories.saved': 'Immich 設定已儲存',
|
||||
'memories.connectionSuccess': '已連線到 {provider_name}',
|
||||
'memories.connectionError': '無法連線到 {provider_name}',
|
||||
'memories.saved': '{provider_name} 設定已儲存',
|
||||
'memories.saveError': '無法儲存 {provider_name} 設定',
|
||||
'memories.oldest': '最早優先',
|
||||
'memories.newest': '最新優先',
|
||||
'memories.allLocations': '所有地點',
|
||||
'memories.addPhotos': '新增照片',
|
||||
'memories.linkAlbum': '關聯相簿',
|
||||
'memories.selectAlbum': '選擇 Immich 相簿',
|
||||
'memories.selectAlbum': '選擇 {provider_name} 相簿',
|
||||
'memories.selectAlbumMultiple': '選擇相簿',
|
||||
'memories.noAlbums': '未找到相簿',
|
||||
'memories.syncAlbum': '同步相簿',
|
||||
'memories.unlinkAlbum': '取消關聯',
|
||||
'memories.photos': '張照片',
|
||||
'memories.selectPhotos': '從 Immich 選擇照片',
|
||||
'memories.selectPhotos': '從 {provider_name} 選擇照片',
|
||||
'memories.selectPhotosMultiple': '選擇照片',
|
||||
'memories.selectHint': '點選照片以選擇。',
|
||||
'memories.selected': '已選擇',
|
||||
'memories.addSelected': '新增 {count} 張照片',
|
||||
@@ -1518,6 +1638,40 @@ const zhTw: Record<string, string> = {
|
||||
'undo.importGpx': 'GPX 匯入',
|
||||
'undo.importGoogleList': 'Google 地圖匯入',
|
||||
|
||||
// Todo
|
||||
'todo.subtab.packing': '行李清單',
|
||||
'todo.subtab.todo': '待辦事項',
|
||||
'todo.completed': '已完成',
|
||||
'todo.filter.all': '全部',
|
||||
'todo.filter.open': '未完成',
|
||||
'todo.filter.done': '已完成',
|
||||
'todo.uncategorized': '未分類',
|
||||
'todo.namePlaceholder': '任務名稱',
|
||||
'todo.descriptionPlaceholder': '說明(可選)',
|
||||
'todo.unassigned': '未指派',
|
||||
'todo.noCategory': '無分類',
|
||||
'todo.hasDescription': '有說明',
|
||||
'todo.addItem': '新增任務...',
|
||||
'todo.newCategory': '分類名稱',
|
||||
'todo.addCategory': '新增分類',
|
||||
'todo.newItem': '新任務',
|
||||
'todo.empty': '尚無任務。新增任務以開始!',
|
||||
'todo.filter.my': '我的任務',
|
||||
'todo.filter.overdue': '已逾期',
|
||||
'todo.sidebar.tasks': '任務',
|
||||
'todo.sidebar.categories': '分類',
|
||||
'todo.detail.title': '任務',
|
||||
'todo.detail.description': '說明',
|
||||
'todo.detail.category': '分類',
|
||||
'todo.detail.dueDate': '到期日',
|
||||
'todo.detail.assignedTo': '指派給',
|
||||
'todo.detail.delete': '刪除',
|
||||
'todo.detail.save': '儲存變更',
|
||||
'todo.sortByPrio': '優先順序',
|
||||
'todo.detail.priority': '優先順序',
|
||||
'todo.detail.noPriority': '無',
|
||||
'todo.detail.create': '建立任務',
|
||||
|
||||
// Notifications
|
||||
'notifications.title': '通知',
|
||||
'notifications.markAllRead': '全部標為已讀',
|
||||
@@ -1554,6 +1708,110 @@ const zhTw: Record<string, string> = {
|
||||
'notifications.test.adminText': '{actor} 向所有管理員傳送了測試通知。',
|
||||
'notifications.test.tripTitle': '{actor} 在您的行程中發帖',
|
||||
'notifications.test.tripText': '行程"{trip}"的測試通知。',
|
||||
'notifications.versionAvailable.title': '有可用更新',
|
||||
'notifications.versionAvailable.text': 'TREK {version} 現已推出。',
|
||||
'notifications.versionAvailable.button': '查看詳情',
|
||||
|
||||
// Notifications — dev test events
|
||||
'notif.test.title': '[測試] 通知',
|
||||
'notif.test.simple.text': '這是一條簡單的測試通知。',
|
||||
'notif.test.boolean.text': '您接受此測試通知嗎?',
|
||||
'notif.test.navigate.text': '點選下方前往儀表板。',
|
||||
|
||||
// Notifications
|
||||
'notif.trip_invite.title': '行程邀請',
|
||||
'notif.trip_invite.text': '{actor} 邀請您加入 {trip}',
|
||||
'notif.booking_change.title': '預訂已更新',
|
||||
'notif.booking_change.text': '{actor} 已更新 {trip} 中的預訂',
|
||||
'notif.trip_reminder.title': '行程提醒',
|
||||
'notif.trip_reminder.text': '您的行程 {trip} 即將開始!',
|
||||
'notif.vacay_invite.title': 'Vacay 合併邀請',
|
||||
'notif.vacay_invite.text': '{actor} 邀請您合併假期計畫',
|
||||
'notif.photos_shared.title': '已分享照片',
|
||||
'notif.photos_shared.text': '{actor} 在 {trip} 中分享了 {count} 張照片',
|
||||
'notif.collab_message.title': '新訊息',
|
||||
'notif.collab_message.text': '{actor} 在 {trip} 中傳送了訊息',
|
||||
'notif.packing_tagged.title': '行李指派',
|
||||
'notif.packing_tagged.text': '{actor} 在 {trip} 中將您指派至 {category}',
|
||||
'notif.version_available.title': '有新版本可用',
|
||||
'notif.version_available.text': 'TREK {version} 現已推出',
|
||||
'notif.action.view_trip': '查看行程',
|
||||
'notif.action.view_collab': '查看訊息',
|
||||
'notif.action.view_packing': '查看行李',
|
||||
'notif.action.view_photos': '查看照片',
|
||||
'notif.action.view_vacay': '查看 Vacay',
|
||||
'notif.action.view_admin': '前往管理員',
|
||||
'notif.action.view': '查看',
|
||||
'notif.action.accept': '接受',
|
||||
'notif.action.decline': '拒絕',
|
||||
'notif.generic.title': '通知',
|
||||
'notif.generic.text': '您有一則新通知',
|
||||
'notif.dev.unknown_event.title': '[DEV] 未知事件',
|
||||
'notif.dev.unknown_event.text': '事件類型「{event}」未在 EVENT_NOTIFICATION_CONFIG 中登錄',
|
||||
|
||||
// OAuth scope groups
|
||||
'oauth.scope.group.trips': '行程',
|
||||
'oauth.scope.group.places': '地點',
|
||||
'oauth.scope.group.atlas': 'Atlas',
|
||||
'oauth.scope.group.packing': '行李',
|
||||
'oauth.scope.group.todos': '待辦事項',
|
||||
'oauth.scope.group.budget': '預算',
|
||||
'oauth.scope.group.reservations': '預訂',
|
||||
'oauth.scope.group.collab': '協作',
|
||||
'oauth.scope.group.notifications': '通知',
|
||||
'oauth.scope.group.vacay': '假期',
|
||||
'oauth.scope.group.geo': 'Geo',
|
||||
'oauth.scope.group.weather': '天氣',
|
||||
|
||||
// OAuth scope labels & descriptions
|
||||
'oauth.scope.trips:read.label': '檢視行程與旅遊計畫',
|
||||
'oauth.scope.trips:read.description': '讀取行程、天數、每日筆記及成員',
|
||||
'oauth.scope.trips:write.label': '編輯行程與旅遊計畫',
|
||||
'oauth.scope.trips:write.description': '建立及更新行程、天數、筆記並管理成員',
|
||||
'oauth.scope.trips:delete.label': '刪除行程',
|
||||
'oauth.scope.trips:delete.description': '永久刪除整個行程——此操作無法復原',
|
||||
'oauth.scope.trips:share.label': '管理分享連結',
|
||||
'oauth.scope.trips:share.description': '建立、更新及撤銷行程的公開分享連結',
|
||||
'oauth.scope.places:read.label': '檢視地點與地圖資料',
|
||||
'oauth.scope.places:read.description': '讀取地點、每日指派、標籤及類別',
|
||||
'oauth.scope.places:write.label': '管理地點',
|
||||
'oauth.scope.places:write.description': '建立、更新及刪除地點、指派及標籤',
|
||||
'oauth.scope.atlas:read.label': '檢視 Atlas',
|
||||
'oauth.scope.atlas:read.description': '讀取已造訪的國家、地區及願望清單',
|
||||
'oauth.scope.atlas:write.label': '管理 Atlas',
|
||||
'oauth.scope.atlas:write.description': '標記已造訪的國家及地區,管理願望清單',
|
||||
'oauth.scope.packing:read.label': '檢視行李清單',
|
||||
'oauth.scope.packing:read.description': '讀取行李物品、行李袋及類別負責人',
|
||||
'oauth.scope.packing:write.label': '管理行李清單',
|
||||
'oauth.scope.packing:write.description': '新增、更新、刪除、勾選及重新排序行李物品和行李袋',
|
||||
'oauth.scope.todos:read.label': '檢視待辦清單',
|
||||
'oauth.scope.todos:read.description': '讀取行程待辦事項及類別負責人',
|
||||
'oauth.scope.todos:write.label': '管理待辦清單',
|
||||
'oauth.scope.todos:write.description': '建立、更新、勾選、刪除及重新排序待辦事項',
|
||||
'oauth.scope.budget:read.label': '檢視預算',
|
||||
'oauth.scope.budget:read.description': '讀取預算項目及費用明細',
|
||||
'oauth.scope.budget:write.label': '管理預算',
|
||||
'oauth.scope.budget:write.description': '建立、更新及刪除預算項目',
|
||||
'oauth.scope.reservations:read.label': '檢視預訂',
|
||||
'oauth.scope.reservations:read.description': '讀取預訂及住宿詳情',
|
||||
'oauth.scope.reservations:write.label': '管理預訂',
|
||||
'oauth.scope.reservations:write.description': '建立、更新、刪除及重新排序預訂',
|
||||
'oauth.scope.collab:read.label': '檢視協作',
|
||||
'oauth.scope.collab:read.description': '讀取協作筆記、投票及訊息',
|
||||
'oauth.scope.collab:write.label': '管理協作',
|
||||
'oauth.scope.collab:write.description': '建立、更新及刪除協作筆記、投票及訊息',
|
||||
'oauth.scope.notifications:read.label': '檢視通知',
|
||||
'oauth.scope.notifications:read.description': '讀取應用程式通知及未讀數量',
|
||||
'oauth.scope.notifications:write.label': '管理通知',
|
||||
'oauth.scope.notifications:write.description': '將通知標為已讀並回覆',
|
||||
'oauth.scope.vacay:read.label': '檢視假期計畫',
|
||||
'oauth.scope.vacay:read.description': '讀取假期計畫資料、項目及統計',
|
||||
'oauth.scope.vacay:write.label': '管理假期計畫',
|
||||
'oauth.scope.vacay:write.description': '建立及管理假期項目、節假日及團隊計畫',
|
||||
'oauth.scope.geo:read.label': '地圖與地理編碼',
|
||||
'oauth.scope.geo:read.description': '搜尋地點、解析地圖 URL 及反向地理編碼坐標',
|
||||
'oauth.scope.weather:read.label': '天氣預報',
|
||||
'oauth.scope.weather:read.description': '取得行程地點及日期的天氣預報',
|
||||
}
|
||||
|
||||
export default zhTw
|
||||
@@ -321,7 +321,7 @@ describe('AdminPage', () => {
|
||||
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: /^users$/i })).toBeInTheDocument());
|
||||
|
||||
expect(screen.queryByRole('button', { name: /mcp tokens/i })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: /mcp access/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows MCP Tokens tab button when MCP addon is enabled', async () => {
|
||||
@@ -337,7 +337,7 @@ describe('AdminPage', () => {
|
||||
render(<AdminPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /mcp tokens/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /mcp access/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -646,9 +646,9 @@ describe('AdminPage', () => {
|
||||
seedStore(useAuthStore, { isAuthenticated: true, user: buildAdmin() });
|
||||
render(<AdminPage />);
|
||||
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: /mcp tokens/i })).toBeInTheDocument());
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: /mcp access/i })).toBeInTheDocument());
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /mcp tokens/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /mcp access/i }));
|
||||
|
||||
expect(screen.getByTestId('mcp-tokens-panel')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useAuthStore } from '../store/authStore'
|
||||
import { oauthApi } from '../api/client'
|
||||
import { SCOPE_GROUPS } from '../api/oauthScopes'
|
||||
import { Lock, ShieldCheck, AlertTriangle, Loader2, LogIn } from 'lucide-react'
|
||||
import { useTranslation } from '../i18n'
|
||||
|
||||
interface ValidateResult {
|
||||
valid: boolean
|
||||
@@ -18,6 +19,7 @@ interface ValidateResult {
|
||||
type PageState = 'loading' | 'login_required' | 'consent' | 'auto_approving' | 'error' | 'done'
|
||||
|
||||
export default function OAuthAuthorizePage(): React.ReactElement {
|
||||
const { t } = useTranslation()
|
||||
const { isAuthenticated, isLoading: authLoading, loadUser } = useAuthStore()
|
||||
const [pageState, setPageState] = useState<PageState>('loading')
|
||||
const [validation, setValidation] = useState<ValidateResult | null>(null)
|
||||
@@ -126,18 +128,18 @@ export default function OAuthAuthorizePage(): React.ReactElement {
|
||||
window.location.href = '/login?redirect=' + encodeURIComponent(next)
|
||||
}
|
||||
|
||||
// Group requested scopes by their human-readable group
|
||||
// Group requested scopes by their translated group name
|
||||
const scopesByGroup = React.useMemo(() => {
|
||||
const requested = validation?.scopes || []
|
||||
const groups: Record<string, string[]> = {}
|
||||
for (const s of requested) {
|
||||
const info = SCOPE_GROUPS[s]
|
||||
const group = info?.group || 'Other'
|
||||
const keys = SCOPE_GROUPS[s]
|
||||
const group = keys ? t(keys.groupKey) : 'Other'
|
||||
if (!groups[group]) groups[group] = []
|
||||
groups[group].push(s)
|
||||
}
|
||||
return groups
|
||||
}, [validation])
|
||||
}, [validation, t])
|
||||
|
||||
// ---- Render states ----
|
||||
|
||||
@@ -270,7 +272,7 @@ export default function OAuthAuthorizePage(): React.ReactElement {
|
||||
</label>
|
||||
<div className="divide-y" style={{ borderColor: 'var(--border-primary)' }}>
|
||||
{groupScopes.map(s => {
|
||||
const info = SCOPE_GROUPS[s]
|
||||
const keys = SCOPE_GROUPS[s]
|
||||
return (
|
||||
<label
|
||||
key={s}
|
||||
@@ -285,8 +287,8 @@ export default function OAuthAuthorizePage(): React.ReactElement {
|
||||
{s.endsWith(':delete') ? '🗑️' : s.endsWith(':write') ? '✏️' : '👁️'}
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>{info?.label || s}</p>
|
||||
<p className="text-xs mt-0.5" style={{ color: 'var(--text-tertiary)' }}>{info?.description || ''}</p>
|
||||
<p className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>{keys ? t(keys.labelKey) : s}</p>
|
||||
<p className="text-xs mt-0.5" style={{ color: 'var(--text-tertiary)' }}>{keys ? t(keys.descriptionKey) : ''}</p>
|
||||
</div>
|
||||
</label>
|
||||
)
|
||||
@@ -304,15 +306,15 @@ export default function OAuthAuthorizePage(): React.ReactElement {
|
||||
<p className="text-xs font-semibold mb-2" style={{ color: 'var(--text-secondary)' }}>{group}</p>
|
||||
<div className="space-y-1.5">
|
||||
{groupScopes.map(s => {
|
||||
const info = SCOPE_GROUPS[s]
|
||||
const keys = SCOPE_GROUPS[s]
|
||||
return (
|
||||
<div key={s} className="flex items-start gap-2.5 px-3 py-2 rounded-lg" style={{ background: 'var(--bg-secondary)' }}>
|
||||
<span className="mt-0.5 text-base leading-none flex-shrink-0">
|
||||
{s.endsWith(':delete') ? '🗑️' : s.endsWith(':write') ? '✏️' : '👁️'}
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>{info?.label || s}</p>
|
||||
<p className="text-xs mt-0.5" style={{ color: 'var(--text-tertiary)' }}>{info?.description || ''}</p>
|
||||
<p className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>{keys ? t(keys.labelKey) : s}</p>
|
||||
<p className="text-xs mt-0.5" style={{ color: 'var(--text-tertiary)' }}>{keys ? t(keys.descriptionKey) : ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
+41
-9
@@ -10,6 +10,7 @@ import { ADDON_IDS } from '../addons';
|
||||
import { registerResources } from './resources';
|
||||
import { registerTools } from './tools';
|
||||
import { McpSession, sessions, revokeUserSessions, revokeUserSessionsForClient } from './sessionManager';
|
||||
import { writeAudit, getClientIp } from '../services/auditLog';
|
||||
|
||||
export { revokeUserSessions, revokeUserSessionsForClient };
|
||||
|
||||
@@ -102,13 +103,14 @@ interface RateLimitEntry {
|
||||
count: number;
|
||||
windowStart: number;
|
||||
}
|
||||
const rateLimitMap = new Map<number, RateLimitEntry>();
|
||||
const rateLimitMap = new Map<string, RateLimitEntry>();
|
||||
|
||||
function isRateLimited(userId: number): boolean {
|
||||
function isRateLimited(userId: number, clientId: string | null): boolean {
|
||||
const key = `${userId}:${clientId ?? 'native'}`;
|
||||
const now = Date.now();
|
||||
const entry = rateLimitMap.get(userId);
|
||||
const entry = rateLimitMap.get(key);
|
||||
if (!entry || now - entry.windowStart > RATE_LIMIT_WINDOW_MS) {
|
||||
rateLimitMap.set(userId, { count: 1, windowStart: now });
|
||||
rateLimitMap.set(key, { count: 1, windowStart: now });
|
||||
return false;
|
||||
}
|
||||
entry.count += 1;
|
||||
@@ -136,13 +138,13 @@ const sessionSweepInterval = setInterval(() => {
|
||||
}
|
||||
}
|
||||
const rateCutoff = Date.now() - RATE_LIMIT_WINDOW_MS;
|
||||
for (const [uid, entry] of rateLimitMap) {
|
||||
if (entry.windowStart < rateCutoff) rateLimitMap.delete(uid);
|
||||
for (const [key, entry] of rateLimitMap) {
|
||||
if (entry.windowStart < rateCutoff) rateLimitMap.delete(key);
|
||||
}
|
||||
if (cleaned > 0 || sessions.size > 0) {
|
||||
console.log(`[MCP] Session sweep: cleaned ${cleaned}, active ${sessions.size}`);
|
||||
}
|
||||
}, 10 * 60 * 1000); // sweep every 10 minutes
|
||||
}, 60 * 1000); // sweep every 1 minute
|
||||
|
||||
// Prevent the interval from keeping the process alive if nothing else is running
|
||||
sessionSweepInterval.unref();
|
||||
@@ -185,6 +187,20 @@ function verifyToken(authHeader: string | undefined): VerifyTokenResult | null {
|
||||
return { user, scopes: null, clientId: null, isStaticToken: false };
|
||||
}
|
||||
|
||||
function logToolCallAudit(req: Request, userId: number, clientId: string | null): void {
|
||||
const body = req.body as Record<string, unknown> | undefined;
|
||||
if (body?.method !== 'tools/call') return;
|
||||
const toolName = (body?.params as Record<string, unknown> | undefined)?.name;
|
||||
if (typeof toolName !== 'string') return;
|
||||
writeAudit({
|
||||
userId,
|
||||
action: 'mcp.tool_call',
|
||||
resource: toolName,
|
||||
details: { clientId: clientId ?? 'native' },
|
||||
ip: getClientIp(req),
|
||||
});
|
||||
}
|
||||
|
||||
export async function mcpHandler(req: Request, res: Response): Promise<void> {
|
||||
if (!isAddonEnabled(ADDON_IDS.MCP)) {
|
||||
res.status(403).json({ error: 'MCP is not enabled' });
|
||||
@@ -198,7 +214,7 @@ export async function mcpHandler(req: Request, res: Response): Promise<void> {
|
||||
}
|
||||
const { user, scopes, clientId, isStaticToken } = tokenResult;
|
||||
|
||||
if (isRateLimited(user.id)) {
|
||||
if (isRateLimited(user.id, clientId)) {
|
||||
res.status(429).json({ error: 'Too many requests. Please slow down.' });
|
||||
return;
|
||||
}
|
||||
@@ -216,7 +232,12 @@ export async function mcpHandler(req: Request, res: Response): Promise<void> {
|
||||
res.status(403).json({ error: 'Session belongs to a different user' });
|
||||
return;
|
||||
}
|
||||
if (session.clientId !== clientId) {
|
||||
res.status(403).json({ error: 'Session was created with a different OAuth client' });
|
||||
return;
|
||||
}
|
||||
session.lastActivity = Date.now();
|
||||
logToolCallAudit(req, user.id, clientId);
|
||||
try {
|
||||
await session.transport.handleRequest(req, res, req.body);
|
||||
} catch (err) {
|
||||
@@ -279,17 +300,28 @@ export async function mcpHandler(req: Request, res: Response): Promise<void> {
|
||||
},
|
||||
});
|
||||
|
||||
logToolCallAudit(req, user.id, clientId);
|
||||
try {
|
||||
await server.connect(transport);
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
} catch (err) {
|
||||
console.error('[MCP] transport.handleRequest error:', err);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({ error: 'Internal MCP error', detail: String(err) });
|
||||
res.status(500).json({ error: 'Internal MCP error' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Invalidate all active MCP sessions (call when addon state changes so sessions re-create with updated tools). */
|
||||
export function invalidateMcpSessions(): void {
|
||||
for (const [sid, session] of sessions) {
|
||||
try { session.server.close(); } catch { /* ignore */ }
|
||||
try { session.transport.close(); } catch { /* ignore */ }
|
||||
sessions.delete(sid);
|
||||
}
|
||||
console.log('[MCP] All sessions invalidated due to addon state change');
|
||||
}
|
||||
|
||||
/** Close all active MCP sessions (call during graceful shutdown). */
|
||||
export function closeMcpSessions(): void {
|
||||
clearInterval(sessionSweepInterval);
|
||||
|
||||
@@ -200,7 +200,7 @@ export function registerResources(server: McpServer, userId: number, scopes: str
|
||||
);
|
||||
|
||||
// Trip to-do list
|
||||
if (isAddonEnabled(ADDON_IDS.PACKING) && canRead(scopes, 'collab')) server.registerResource(
|
||||
if (isAddonEnabled(ADDON_IDS.PACKING) && canRead(scopes, 'todos')) server.registerResource(
|
||||
'trip-todos',
|
||||
new ResourceTemplate('trek://trips/{tripId}/todos', { list: undefined }),
|
||||
{ description: 'To-do items for a trip, ordered by position', mimeType: 'application/json' },
|
||||
@@ -224,7 +224,7 @@ export function registerResources(server: McpServer, userId: number, scopes: str
|
||||
);
|
||||
|
||||
// User's bucket list
|
||||
if (isAddonEnabled(ADDON_IDS.ATLAS) && canRead(scopes, 'places')) server.registerResource(
|
||||
if (isAddonEnabled(ADDON_IDS.ATLAS) && canRead(scopes, 'atlas')) server.registerResource(
|
||||
'bucket-list',
|
||||
'trek://bucket-list',
|
||||
{ description: 'Your personal travel bucket list', mimeType: 'application/json' },
|
||||
@@ -235,7 +235,7 @@ export function registerResources(server: McpServer, userId: number, scopes: str
|
||||
);
|
||||
|
||||
// User's visited countries
|
||||
if (isAddonEnabled(ADDON_IDS.ATLAS) && canRead(scopes, 'places')) server.registerResource(
|
||||
if (isAddonEnabled(ADDON_IDS.ATLAS) && canRead(scopes, 'atlas')) server.registerResource(
|
||||
'visited-countries',
|
||||
'trek://visited-countries',
|
||||
{ description: 'Countries you have marked as visited in Atlas', mimeType: 'application/json' },
|
||||
@@ -296,7 +296,7 @@ export function registerResources(server: McpServer, userId: number, scopes: str
|
||||
);
|
||||
|
||||
// Atlas stats and regions (addon-gated)
|
||||
if (isAddonEnabled(ADDON_IDS.ATLAS) && canRead(scopes, 'places')) {
|
||||
if (isAddonEnabled(ADDON_IDS.ATLAS) && canRead(scopes, 'atlas')) {
|
||||
server.registerResource(
|
||||
'atlas-stats',
|
||||
'trek://atlas/stats',
|
||||
|
||||
+29
-19
@@ -9,8 +9,12 @@ export const SCOPES = {
|
||||
TRIPS_SHARE: 'trips:share',
|
||||
PLACES_READ: 'places:read',
|
||||
PLACES_WRITE: 'places:write',
|
||||
ATLAS_READ: 'atlas:read',
|
||||
ATLAS_WRITE: 'atlas:write',
|
||||
PACKING_READ: 'packing:read',
|
||||
PACKING_WRITE: 'packing:write',
|
||||
TODOS_READ: 'todos:read',
|
||||
TODOS_WRITE: 'todos:write',
|
||||
BUDGET_READ: 'budget:read',
|
||||
BUDGET_WRITE: 'budget:write',
|
||||
RESERVATIONS_READ: 'reservations:read',
|
||||
@@ -21,7 +25,8 @@ export const SCOPES = {
|
||||
NOTIFICATIONS_WRITE: 'notifications:write',
|
||||
VACAY_READ: 'vacay:read',
|
||||
VACAY_WRITE: 'vacay:write',
|
||||
MEDIA_READ: 'media:read',
|
||||
GEO_READ: 'geo:read',
|
||||
WEATHER_READ: 'weather:read',
|
||||
} as const;
|
||||
|
||||
export type Scope = typeof SCOPES[keyof typeof SCOPES];
|
||||
@@ -36,24 +41,29 @@ export interface ScopeInfo {
|
||||
|
||||
export const SCOPE_INFO: Record<Scope, ScopeInfo> = {
|
||||
'trips:read': { label: 'View trips & itineraries', description: 'Read trips, days, day notes, and members', group: 'Trips' },
|
||||
'trips:write': { label: 'Edit trips & itineraries', description: 'Create and update trips, days, notes, and manage members', group: 'Trips' },
|
||||
'trips:delete': { label: 'Delete trips', description: 'Permanently delete entire trips — this action is irreversible', group: 'Trips' },
|
||||
'trips:share': { label: 'Manage share links', description: 'Create, update, and revoke public share links for trips', group: 'Trips' },
|
||||
'places:read': { label: 'View places & map data', description: 'Read places, day assignments, tags, categories, and visited countries', group: 'Places' },
|
||||
'places:write': { label: 'Manage places', description: 'Create, update, and delete places, assignments, tags, and atlas entries', group: 'Places' },
|
||||
'packing:read': { label: 'View packing lists', description: 'Read packing items, bags, and category assignees', group: 'Packing' },
|
||||
'packing:write': { label: 'Manage packing lists', description: 'Add, update, delete, toggle, and reorder packing items and bags', group: 'Packing' },
|
||||
'budget:read': { label: 'View budget', description: 'Read budget items and expense breakdown', group: 'Budget' },
|
||||
'budget:write': { label: 'Manage budget', description: 'Create, update, and delete budget items', group: 'Budget' },
|
||||
'reservations:read': { label: 'View reservations', description: 'Read reservations and accommodation details', group: 'Reservations' },
|
||||
'reservations:write': { label: 'Manage reservations', description: 'Create, update, delete, and reorder reservations', group: 'Reservations' },
|
||||
'collab:read': { label: 'View collaboration', description: 'Read collab notes, polls, messages, and to-do items', group: 'Collaboration' },
|
||||
'collab:write': { label: 'Manage collaboration', description: 'Create, update, and delete collab notes, todos, polls, and messages', group: 'Collaboration' },
|
||||
'notifications:read': { label: 'View notifications', description: 'Read in-app notifications and unread counts', group: 'Notifications' },
|
||||
'notifications:write': { label: 'Manage notifications', description: 'Mark notifications as read and respond to them', group: 'Notifications' },
|
||||
'vacay:read': { label: 'View vacation plans', description: 'Read vacation planning data, entries, and stats', group: 'Vacation' },
|
||||
'vacay:write': { label: 'Manage vacation plans', description: 'Create and manage vacation entries, holidays, and team plans', group: 'Vacation' },
|
||||
'media:read': { label: 'Maps & weather data', description: 'Search locations, resolve map URLs, and fetch weather forecasts', group: 'Media' },
|
||||
'trips:write': { label: 'Edit trips & itineraries', description: 'Create and update trips, days, notes, and manage members', group: 'Trips' },
|
||||
'trips:delete': { label: 'Delete trips', description: 'Permanently delete entire trips — this action is irreversible', group: 'Trips' },
|
||||
'trips:share': { label: 'Manage share links', description: 'Create, update, and revoke public share links for trips', group: 'Trips' },
|
||||
'places:read': { label: 'View places & map data', description: 'Read places, day assignments, tags, and categories', group: 'Places' },
|
||||
'places:write': { label: 'Manage places', description: 'Create, update, and delete places, assignments, and tags', group: 'Places' },
|
||||
'atlas:read': { label: 'View Atlas', description: 'Read visited countries, regions, and bucket list', group: 'Atlas' },
|
||||
'atlas:write': { label: 'Manage Atlas', description: 'Mark countries and regions visited, manage bucket list', group: 'Atlas' },
|
||||
'packing:read': { label: 'View packing lists', description: 'Read packing items, bags, and category assignees', group: 'Packing' },
|
||||
'packing:write': { label: 'Manage packing lists', description: 'Add, update, delete, toggle, and reorder packing items and bags', group: 'Packing' },
|
||||
'todos:read': { label: 'View to-do lists', description: 'Read trip to-do items and category assignees', group: 'To-dos' },
|
||||
'todos:write': { label: 'Manage to-do lists', description: 'Create, update, toggle, delete, and reorder to-do items', group: 'To-dos' },
|
||||
'budget:read': { label: 'View budget', description: 'Read budget items and expense breakdown', group: 'Budget' },
|
||||
'budget:write': { label: 'Manage budget', description: 'Create, update, and delete budget items', group: 'Budget' },
|
||||
'reservations:read': { label: 'View reservations', description: 'Read reservations and accommodation details', group: 'Reservations' },
|
||||
'reservations:write': { label: 'Manage reservations', description: 'Create, update, delete, and reorder reservations', group: 'Reservations' },
|
||||
'collab:read': { label: 'View collaboration', description: 'Read collab notes, polls, and messages', group: 'Collaboration' },
|
||||
'collab:write': { label: 'Manage collaboration', description: 'Create, update, and delete collab notes, polls, and messages', group: 'Collaboration' },
|
||||
'notifications:read': { label: 'View notifications', description: 'Read in-app notifications and unread counts', group: 'Notifications' },
|
||||
'notifications:write': { label: 'Manage notifications', description: 'Mark notifications as read and respond to them', group: 'Notifications' },
|
||||
'vacay:read': { label: 'View vacation plans', description: 'Read vacation planning data, entries, and stats', group: 'Vacation' },
|
||||
'vacay:write': { label: 'Manage vacation plans', description: 'Create and manage vacation entries, holidays, and team plans', group: 'Vacation' },
|
||||
'geo:read': { label: 'Maps & geocoding', description: 'Search locations, resolve map URLs, and reverse geocode coordinates', group: 'Geo' },
|
||||
'weather:read': { label: 'Weather forecasts', description: 'Fetch weather forecasts for trip locations and dates', group: 'Weather' },
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -2,7 +2,7 @@ import { broadcast } from '../../websocket';
|
||||
|
||||
export function safeBroadcast(tripId: number, event: string, payload: Record<string, unknown>): void {
|
||||
try {
|
||||
broadcast(tripId, event, payload);
|
||||
broadcast(tripId, event, { ...payload, _source: 'mcp' });
|
||||
} catch (err) {
|
||||
console.error(`[MCP] broadcast failed for ${event}:`, err?.message ?? err);
|
||||
}
|
||||
|
||||
@@ -111,6 +111,8 @@ export function registerAssignmentTools(server: McpServer, userId: number, scope
|
||||
async ({ tripId, assignmentId, newDayId, oldDayId, orderIndex }) => {
|
||||
if (isDemoUser(userId)) return demoDenied();
|
||||
if (!canAccessTrip(tripId, userId)) return noAccess();
|
||||
if (!getAssignmentForTrip(assignmentId, tripId)) return { content: [{ type: 'text' as const, text: 'Assignment not found.' }], isError: true };
|
||||
if (!getDay(newDayId, tripId)) return { content: [{ type: 'text' as const, text: 'Day not found.' }], isError: true };
|
||||
const result = moveAssignment(assignmentId, newDayId, orderIndex ?? 0, oldDayId);
|
||||
safeBroadcast(tripId, 'assignment:moved', { assignment: result.assignment, oldDayId: result.oldDayId });
|
||||
return ok({ assignment: result.assignment });
|
||||
@@ -129,6 +131,7 @@ export function registerAssignmentTools(server: McpServer, userId: number, scope
|
||||
},
|
||||
async ({ tripId, assignmentId }) => {
|
||||
if (!canAccessTrip(tripId, userId)) return noAccess();
|
||||
if (!getAssignmentForTrip(assignmentId, tripId)) return { content: [{ type: 'text' as const, text: 'Assignment not found.' }], isError: true };
|
||||
const participants = getAssignmentParticipants(assignmentId);
|
||||
return ok({ participants });
|
||||
}
|
||||
@@ -148,6 +151,7 @@ export function registerAssignmentTools(server: McpServer, userId: number, scope
|
||||
async ({ tripId, assignmentId, userIds }) => {
|
||||
if (isDemoUser(userId)) return demoDenied();
|
||||
if (!canAccessTrip(tripId, userId)) return noAccess();
|
||||
if (!getAssignmentForTrip(assignmentId, tripId)) return { content: [{ type: 'text' as const, text: 'Assignment not found.' }], isError: true };
|
||||
const participants = setAssignmentParticipants(assignmentId, userIds);
|
||||
safeBroadcast(tripId, 'assignment:participants', { assignmentId, participants });
|
||||
return ok({ participants });
|
||||
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
import { canRead, canWrite } from '../scopes';
|
||||
|
||||
export function registerAtlasTools(server: McpServer, userId: number, scopes: string[] | null): void {
|
||||
const R = canRead(scopes, 'places');
|
||||
const W = canWrite(scopes, 'places');
|
||||
const R = canRead(scopes, 'atlas');
|
||||
const W = canWrite(scopes, 'atlas');
|
||||
|
||||
if (!isAddonEnabled(ADDON_IDS.ATLAS)) return;
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@ export function registerDayTools(server: McpServer, userId: number, scopes: stri
|
||||
async ({ tripId, dayId }) => {
|
||||
if (isDemoUser(userId)) return demoDenied();
|
||||
if (!canAccessTrip(tripId, userId)) return noAccess();
|
||||
if (!getDay(dayId, tripId)) return { content: [{ type: 'text' as const, text: 'Day not found.' }], isError: true };
|
||||
deleteDay(dayId);
|
||||
safeBroadcast(tripId, 'day:deleted', { id: dayId });
|
||||
return ok({ success: true });
|
||||
@@ -152,6 +153,7 @@ export function registerDayTools(server: McpServer, userId: number, scopes: stri
|
||||
async ({ tripId, accommodationId }) => {
|
||||
if (isDemoUser(userId)) return demoDenied();
|
||||
if (!canAccessTrip(tripId, userId)) return noAccess();
|
||||
if (!getAccommodation(accommodationId, tripId)) return { content: [{ type: 'text' as const, text: 'Accommodation not found.' }], isError: true };
|
||||
const { linkedReservationId } = deleteAccommodation(accommodationId);
|
||||
safeBroadcast(tripId, 'accommodation:deleted', { id: accommodationId, linkedReservationId });
|
||||
return ok({ success: true, linkedReservationId });
|
||||
|
||||
@@ -9,11 +9,12 @@ import {
|
||||
import { canRead } from '../scopes';
|
||||
|
||||
export function registerMapsWeatherTools(server: McpServer, userId: number, scopes: string[] | null): void {
|
||||
if (!canRead(scopes, 'media')) return;
|
||||
const canGeo = canRead(scopes, 'geo');
|
||||
const canWeather = canRead(scopes, 'weather');
|
||||
|
||||
// --- MAPS EXTRAS ---
|
||||
|
||||
server.registerTool(
|
||||
if (canGeo) server.registerTool(
|
||||
'get_place_details',
|
||||
{
|
||||
description: 'Fetch detailed information about a place by its Google Place ID.',
|
||||
@@ -30,7 +31,7 @@ export function registerMapsWeatherTools(server: McpServer, userId: number, scop
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
if (canGeo) server.registerTool(
|
||||
'reverse_geocode',
|
||||
{
|
||||
description: 'Get a human-readable address for given coordinates.',
|
||||
@@ -48,7 +49,7 @@ export function registerMapsWeatherTools(server: McpServer, userId: number, scop
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
if (canGeo) server.registerTool(
|
||||
'resolve_maps_url',
|
||||
{
|
||||
description: 'Resolve a Google Maps share URL to coordinates and place name.',
|
||||
@@ -66,7 +67,7 @@ export function registerMapsWeatherTools(server: McpServer, userId: number, scop
|
||||
|
||||
// --- WEATHER ---
|
||||
|
||||
server.registerTool(
|
||||
if (canWeather) server.registerTool(
|
||||
'get_weather',
|
||||
{
|
||||
description: 'Get weather forecast for a location and date.',
|
||||
@@ -88,7 +89,7 @@ export function registerMapsWeatherTools(server: McpServer, userId: number, scop
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
if (canWeather) server.registerTool(
|
||||
'get_detailed_weather',
|
||||
{
|
||||
description: 'Get hourly/detailed weather forecast for a location and date.',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
|
||||
import { z } from 'zod';
|
||||
import { isDemoUser } from '../../services/authService';
|
||||
import { listTags, createTag, updateTag, deleteTag } from '../../services/tagService';
|
||||
import { listTags, createTag, getTagByIdAndUser, updateTag, deleteTag } from '../../services/tagService';
|
||||
import {
|
||||
TOOL_ANNOTATIONS_READONLY, TOOL_ANNOTATIONS_WRITE,
|
||||
TOOL_ANNOTATIONS_DELETE, TOOL_ANNOTATIONS_NON_IDEMPOTENT,
|
||||
@@ -58,6 +58,7 @@ export function registerTagTools(server: McpServer, userId: number, scopes: stri
|
||||
},
|
||||
async ({ tagId, name, color }) => {
|
||||
if (isDemoUser(userId)) return demoDenied();
|
||||
if (!getTagByIdAndUser(tagId, userId)) return { content: [{ type: 'text' as const, text: 'Tag not found.' }], isError: true };
|
||||
const tag = updateTag(tagId, name, color);
|
||||
if (!tag) return { content: [{ type: 'text' as const, text: 'Tag not found.' }], isError: true };
|
||||
return ok({ tag });
|
||||
@@ -75,6 +76,7 @@ export function registerTagTools(server: McpServer, userId: number, scopes: stri
|
||||
},
|
||||
async ({ tagId }) => {
|
||||
if (isDemoUser(userId)) return demoDenied();
|
||||
if (!getTagByIdAndUser(tagId, userId)) return { content: [{ type: 'text' as const, text: 'Tag not found.' }], isError: true };
|
||||
deleteTag(tagId);
|
||||
return ok({ success: true });
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ import { isAddonEnabled } from '../../services/adminService';
|
||||
import { ADDON_IDS } from '../../addons';
|
||||
|
||||
export function registerTodoTools(server: McpServer, userId: number, scopes: string[] | null): void {
|
||||
const R = canRead(scopes, 'collab');
|
||||
const W = canWrite(scopes, 'collab');
|
||||
const R = canRead(scopes, 'todos');
|
||||
const W = canWrite(scopes, 'todos');
|
||||
|
||||
if (!isAddonEnabled(ADDON_IDS.PACKING)) return;
|
||||
|
||||
|
||||
@@ -167,8 +167,9 @@ export function registerTripTools(server: McpServer, userId: number, scopes: str
|
||||
const canReadBudget = budgetEnabled && canRead(scopes, 'budget');
|
||||
const canReadPacking = packingEnabled && canRead(scopes, 'packing');
|
||||
const canReadCollab = collabEnabled && canRead(scopes, 'collab');
|
||||
const canReadTodos = packingEnabled && canRead(scopes, 'todos');
|
||||
const canReadRes = canRead(scopes, 'reservations');
|
||||
const todos = canReadPacking ? listTodoItems(tripId) : [];
|
||||
const todos = canReadTodos ? listTodoItems(tripId) : [];
|
||||
let pollCount = 0;
|
||||
let messageCount = 0;
|
||||
if (canReadCollab) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { authenticate, adminOnly } from '../middleware/auth';
|
||||
import { AuthRequest } from '../types';
|
||||
import { writeAudit, getClientIp, logInfo } from '../services/auditLog';
|
||||
import * as svc from '../services/adminService';
|
||||
import { invalidateMcpSessions } from '../mcp';
|
||||
import { getPreferencesMatrix, setAdminPreferences } from '../services/notificationPreferencesService';
|
||||
|
||||
const router = express.Router();
|
||||
@@ -292,6 +293,8 @@ router.put('/addons/:id', (req: Request, res: Response) => {
|
||||
ip: getClientIp(req),
|
||||
details: result.auditDetails,
|
||||
});
|
||||
// Invalidate all MCP sessions so they re-create with the updated addon tool set
|
||||
invalidateMcpSessions();
|
||||
res.json({ addon: result.addon });
|
||||
});
|
||||
|
||||
|
||||
@@ -193,8 +193,11 @@ oauthPublicRouter.post('/oauth/register', dcrLimiter, (req: Request, res: Respon
|
||||
const authMethod = typeof body.token_endpoint_auth_method === 'string' ? body.token_endpoint_auth_method : 'client_secret_post';
|
||||
const isPublic = authMethod === 'none';
|
||||
|
||||
// Resolve requested scopes — default to all supported scopes if not specified
|
||||
const rawScope = typeof body.scope === 'string' ? body.scope : ALL_SCOPES.join(' ');
|
||||
// Resolve requested scopes — scope is required; no implicit full-access grant
|
||||
if (typeof body.scope !== 'string' || body.scope.trim() === '') {
|
||||
return res.status(400).json({ error: 'invalid_client_metadata', error_description: 'scope is required' });
|
||||
}
|
||||
const rawScope = body.scope;
|
||||
const requestedScopes = rawScope.split(' ').filter(s => (ALL_SCOPES as string[]).includes(s));
|
||||
if (requestedScopes.length === 0) {
|
||||
return res.status(400).json({ error: 'invalid_client_metadata', error_description: 'No valid scopes requested' });
|
||||
@@ -351,6 +354,10 @@ oauthApiRouter.post('/authorize', requireCookieAuth, (req: Request, res: Respons
|
||||
codeChallengeMethod: 'S256',
|
||||
});
|
||||
|
||||
if (!code) {
|
||||
return res.status(503).json({ error: 'server_error', error_description: 'Authorization server is temporarily unavailable' });
|
||||
}
|
||||
|
||||
const url = new URL(redirect_uri);
|
||||
url.searchParams.set('code', code);
|
||||
if (state) url.searchParams.set('state', state);
|
||||
|
||||
@@ -33,6 +33,7 @@ interface PendingCode {
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
const MAX_PENDING_CODES = 500;
|
||||
const pendingCodes = new Map<string, PendingCode>();
|
||||
|
||||
setInterval(() => {
|
||||
@@ -89,11 +90,11 @@ function timingSafeEqualHex(a: string, b: string): boolean {
|
||||
}
|
||||
|
||||
function generateAccessToken(): string {
|
||||
return 'trekoa_' + randomBytes(24).toString('hex');
|
||||
return 'trekoa_' + randomBytes(32).toString('hex');
|
||||
}
|
||||
|
||||
function generateRefreshToken(): string {
|
||||
return 'trekrf_' + randomBytes(24).toString('hex');
|
||||
return 'trekrf_' + randomBytes(32).toString('hex');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -244,7 +245,8 @@ export function createAuthCode(params: {
|
||||
scopes: string[];
|
||||
codeChallenge: string;
|
||||
codeChallengeMethod: 'S256';
|
||||
}): string {
|
||||
}): string | null {
|
||||
if (pendingCodes.size >= MAX_PENDING_CODES) return null;
|
||||
const rawCode = randomBytes(32).toString('hex');
|
||||
pendingCodes.set(rawCode, { ...params, expiresAt: Date.now() + AUTH_CODE_TTL_MS });
|
||||
return rawCode;
|
||||
|
||||
@@ -24,14 +24,21 @@ describe('ALL_SCOPES', () => {
|
||||
expect(ALL_SCOPES).toContain('trips:write');
|
||||
expect(ALL_SCOPES).toContain('trips:delete');
|
||||
expect(ALL_SCOPES).toContain('trips:share');
|
||||
expect(ALL_SCOPES).toContain('places:read');
|
||||
expect(ALL_SCOPES).toContain('places:write');
|
||||
expect(ALL_SCOPES).toContain('atlas:read');
|
||||
expect(ALL_SCOPES).toContain('atlas:write');
|
||||
expect(ALL_SCOPES).toContain('budget:read');
|
||||
expect(ALL_SCOPES).toContain('budget:write');
|
||||
expect(ALL_SCOPES).toContain('packing:read');
|
||||
expect(ALL_SCOPES).toContain('packing:write');
|
||||
expect(ALL_SCOPES).toContain('todos:read');
|
||||
expect(ALL_SCOPES).toContain('todos:write');
|
||||
expect(ALL_SCOPES).toContain('collab:read');
|
||||
expect(ALL_SCOPES).toContain('collab:write');
|
||||
expect(ALL_SCOPES).toContain('places:read');
|
||||
expect(ALL_SCOPES).toContain('places:write');
|
||||
expect(ALL_SCOPES).toContain('geo:read');
|
||||
expect(ALL_SCOPES).toContain('weather:read');
|
||||
expect(ALL_SCOPES).not.toContain('media:read');
|
||||
});
|
||||
|
||||
it('is a non-empty array', () => {
|
||||
|
||||
@@ -131,7 +131,7 @@ describe('Tool: delete_day', () => {
|
||||
});
|
||||
const data = parseToolResult(result) as any;
|
||||
expect(data.success).toBe(true);
|
||||
expect(broadcastMock).toHaveBeenCalledWith(trip.id, 'day:deleted', { id: day.id });
|
||||
expect(broadcastMock).toHaveBeenCalledWith(trip.id, 'day:deleted', expect.objectContaining({ id: day.id }));
|
||||
expect(testDb.prepare('SELECT id FROM days WHERE id = ?').get(day.id)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user