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:
jubnl
2026-04-11 02:06:09 +02:00
parent 4670d4914c
commit 535c06bb3f
39 changed files with 1930 additions and 237 deletions
+37 -25
View File
@@ -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
}
+7 -1
View File
@@ -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)
}
}
+14 -4
View File
@@ -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(() => {
+111 -6
View File
@@ -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
+114 -9
View File
@@ -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
+112 -7
View File
@@ -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
+116 -15
View File
@@ -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
+64
View File
@@ -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
+118 -13
View File
@@ -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
+111 -6
View File
@@ -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
+111 -6
View File
@@ -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
+113 -8
View File
@@ -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
+123 -18
View File
@@ -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
+118 -13
View File
@@ -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
+111 -6
View File
@@ -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
+111 -6
View File
@@ -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。需要 HTTPSlocalhost 除外)。要求精确匹配。',
'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
+274 -16
View File
@@ -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。需要 HTTPSlocalhost 除外)。需要完全符合。',
'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
+4 -4
View File
@@ -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();
});
+12 -10
View File
@@ -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
View File
@@ -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);
+4 -4
View File
@@ -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
View File
@@ -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' },
};
// ---------------------------------------------------------------------------
+1 -1
View File
@@ -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);
}
+4
View File
@@ -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 });
+2 -2
View File
@@ -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;
+2
View File
@@ -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 });
+7 -6
View File
@@ -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.',
+3 -1
View File
@@ -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 });
}
+2 -2
View File
@@ -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;
+2 -1
View File
@@ -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
View File
@@ -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 });
});
+9 -2
View File
@@ -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);
+5 -3
View File
@@ -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;
+9 -2
View File
@@ -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();
});
});