diff --git a/client/src/api/oauthScopes.ts b/client/src/api/oauthScopes.ts index 53ae8fdd..55cc3c09 100644 --- a/client/src/api/oauthScopes.ts +++ b/client/src/api/oauthScopes.ts @@ -7,38 +7,50 @@ export interface ScopeInfo { group: string } -export const SCOPE_GROUPS: Record = { - '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 = { + '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> { +export function getScopesByGroup(t: (key: string) => string): Record> { const groups: Record> = {} - 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 } diff --git a/client/src/components/Collab/CollabChat.tsx b/client/src/components/Collab/CollabChat.tsx index 251a4439..cb9711a6 100644 --- a/client/src/components/Collab/CollabChat.tsx +++ b/client/src/components/Collab/CollabChat.tsx @@ -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[]>([]) + + 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) => { diff --git a/client/src/components/Collab/WhatsNextWidget.tsx b/client/src/components/Collab/WhatsNextWidget.tsx index c5fd11a2..90d39caf 100644 --- a/client/src/components/Collab/WhatsNextWidget.tsx +++ b/client/src/components/Collab/WhatsNextWidget.tsx @@ -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' }) } diff --git a/client/src/components/OAuth/ScopeGroupPicker.tsx b/client/src/components/OAuth/ScopeGroupPicker.tsx index 8d7e8c18..aa69828b 100644 --- a/client/src/components/OAuth/ScopeGroupPicker.tsx +++ b/client/src/components/OAuth/ScopeGroupPicker.tsx @@ -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>({}) + 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')}
diff --git a/client/src/components/Settings/IntegrationsTab.tsx b/client/src/components/Settings/IntegrationsTab.tsx index 9516dc2c..430da0f6 100644 --- a/client/src/components/Settings/IntegrationsTab.tsx +++ b/client/src/components/Settings/IntegrationsTab.tsx @@ -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(null) const [copiedKey, setCopiedKey] = useState(null) + const copyTimerRef = useRef | 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) }) } diff --git a/client/src/components/Trips/TripMembersModal.tsx b/client/src/components/Trips/TripMembersModal.tsx index 47a6b548..7ef8cfed 100644 --- a/client/src/components/Trips/TripMembersModal.tsx +++ b/client/src/components/Trips/TripMembersModal.tsx @@ -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 | 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) } } diff --git a/client/src/components/shared/Toast.tsx b/client/src/components/shared/Toast.tsx index 29ceb601..28d47e28 100644 --- a/client/src/components/shared/Toast.tsx +++ b/client/src/components/shared/Toast.tsx @@ -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 = { export function ToastContainer() { const [toasts, setToasts] = useState([]) + const timersRef = useRef[]>([]) + + 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(() => { diff --git a/client/src/i18n/translations/ar.ts b/client/src/i18n/translations/ar.ts index ef3c216f..5791f64c 100644 --- a/client/src/i18n/translations/ar.ts +++ b/client/src/i18n/translations/ar.ts @@ -184,9 +184,6 @@ const ar: Record = { '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 = { 'settings.mcp.endpoint': 'نقطة نهاية MCP', 'settings.mcp.clientConfig': 'إعداد العميل', 'settings.mcp.clientConfigHint': 'استبدل برمز API من القائمة أدناه. قد يحتاج مسار npx إلى ضبط وفق نظامك (مثلاً C:\\PROGRA~1\\nodejs\\npx.cmd على Windows).', - 'settings.mcp.clientConfigHintOAuth': 'Replace and 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': 'استبدل و ببيانات الاعتماد المعروضة في عميل 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 = { '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 = { 'budget.totalBudget': 'إجمالي الميزانية', 'budget.byCategory': 'حسب الفئة', 'budget.editTooltip': 'انقر للتعديل', + 'budget.linkedToReservation': 'مرتبط بحجز — عدّل الاسم هناك', 'budget.confirm.deleteCategory': 'هل تريد حذف الفئة "{name}" مع {count} إدخالات؟', 'budget.deleteCategory': 'حذف الفئة', 'budget.perPerson': 'لكل شخص', @@ -1122,6 +1162,9 @@ const ar: Record = { '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 = { '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 = { '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 diff --git a/client/src/i18n/translations/br.ts b/client/src/i18n/translations/br.ts index 813ccb72..35cc1c1a 100644 --- a/client/src/i18n/translations/br.ts +++ b/client/src/i18n/translations/br.ts @@ -179,9 +179,6 @@ const br: Record = { '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 = { 'settings.mcp.endpoint': 'Endpoint MCP', 'settings.mcp.clientConfig': 'Configuração do cliente', 'settings.mcp.clientConfigHint': 'Substitua 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 and 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 e 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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 diff --git a/client/src/i18n/translations/cs.ts b/client/src/i18n/translations/cs.ts index e6b61b19..43a01d9b 100644 --- a/client/src/i18n/translations/cs.ts +++ b/client/src/i18n/translations/cs.ts @@ -181,7 +181,7 @@ const cs: Record = { 'settings.mcp.endpoint': 'MCP endpoint', 'settings.mcp.clientConfig': 'Konfigurace klienta', 'settings.mcp.clientConfigHint': 'Nahraďte 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 and 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 a 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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 diff --git a/client/src/i18n/translations/de.ts b/client/src/i18n/translations/de.ts index 469b43ef..86c88395 100644 --- a/client/src/i18n/translations/de.ts +++ b/client/src/i18n/translations/de.ts @@ -179,9 +179,6 @@ const de: Record = { '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 = { 'settings.mcp.endpoint': 'MCP-Endpunkt', 'settings.mcp.clientConfig': 'Client-Konfiguration', 'settings.mcp.clientConfigHint': 'Ersetze 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 and 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 und 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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { // 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 = { // 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 = { '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 diff --git a/client/src/i18n/translations/en.ts b/client/src/i18n/translations/en.ts index a3d9d176..27ddaad6 100644 --- a/client/src/i18n/translations/en.ts +++ b/client/src/i18n/translations/en.ts @@ -1753,6 +1753,70 @@ const en: Record = { '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 diff --git a/client/src/i18n/translations/es.ts b/client/src/i18n/translations/es.ts index 94ab8d13..ea643e50 100644 --- a/client/src/i18n/translations/es.ts +++ b/client/src/i18n/translations/es.ts @@ -180,9 +180,6 @@ const es: Record = { '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 = { 'settings.mcp.endpoint': 'Endpoint MCP', 'settings.mcp.clientConfig': 'Configuración del cliente', 'settings.mcp.clientConfigHint': 'Reemplaza 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 and 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 y 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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 diff --git a/client/src/i18n/translations/fr.ts b/client/src/i18n/translations/fr.ts index 778ae84a..bf06c73b 100644 --- a/client/src/i18n/translations/fr.ts +++ b/client/src/i18n/translations/fr.ts @@ -179,9 +179,6 @@ const fr: Record = { '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 = { 'settings.mcp.endpoint': 'Point de terminaison MCP', 'settings.mcp.clientConfig': 'Configuration du client', 'settings.mcp.clientConfigHint': 'Remplacez 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 and 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 et 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 = { '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 = { '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 = { '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 = { '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 = { '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 diff --git a/client/src/i18n/translations/hu.ts b/client/src/i18n/translations/hu.ts index 5dedf5f1..080faece 100644 --- a/client/src/i18n/translations/hu.ts +++ b/client/src/i18n/translations/hu.ts @@ -180,7 +180,7 @@ const hu: Record = { 'settings.mcp.endpoint': 'MCP végpont', 'settings.mcp.clientConfig': 'Kliens konfiguráció', 'settings.mcp.clientConfigHint': 'Cserélje ki a 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 and 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 és 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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 diff --git a/client/src/i18n/translations/it.ts b/client/src/i18n/translations/it.ts index beaa728a..e5692c01 100644 --- a/client/src/i18n/translations/it.ts +++ b/client/src/i18n/translations/it.ts @@ -202,6 +202,48 @@ const it: Record = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 diff --git a/client/src/i18n/translations/nl.ts b/client/src/i18n/translations/nl.ts index d202e2e3..1b2f5430 100644 --- a/client/src/i18n/translations/nl.ts +++ b/client/src/i18n/translations/nl.ts @@ -179,9 +179,6 @@ const nl: Record = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 diff --git a/client/src/i18n/translations/pl.ts b/client/src/i18n/translations/pl.ts index 83c55cdf..aa418719 100644 --- a/client/src/i18n/translations/pl.ts +++ b/client/src/i18n/translations/pl.ts @@ -29,7 +29,7 @@ const pl: Record = { '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 = { 'settings.mcp.endpoint': 'Endpoint MCP', 'settings.mcp.clientConfig': 'Konfiguracja klienta', 'settings.mcp.clientConfigHint': 'Zastąp 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 and 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 i 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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 diff --git a/client/src/i18n/translations/ru.ts b/client/src/i18n/translations/ru.ts index 9ec079a8..515ebc54 100644 --- a/client/src/i18n/translations/ru.ts +++ b/client/src/i18n/translations/ru.ts @@ -179,9 +179,6 @@ const ru: Record = { '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 = { 'settings.mcp.endpoint': 'MCP-эндпоинт', 'settings.mcp.clientConfig': 'Конфигурация клиента', 'settings.mcp.clientConfigHint': 'Замените на API-токен из списка ниже. Путь к npx может потребовать настройки для вашей системы (например, C:\\PROGRA~1\\nodejs\\npx.cmd в Windows).', - 'settings.mcp.clientConfigHintOAuth': 'Replace and 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': 'Замените и на учётные данные из созданного выше клиента 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 = { '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 = { 'budget.totalBudget': 'Общий бюджет', 'budget.byCategory': 'По категориям', 'budget.editTooltip': 'Нажмите для редактирования', + 'budget.linkedToReservation': 'Связано с бронированием — редактируйте название там', 'budget.confirm.deleteCategory': 'Вы уверены, что хотите удалить категорию «{name}» с {count} записями?', 'budget.deleteCategory': 'Удалить категорию', 'budget.perPerson': 'На человека', @@ -1116,6 +1156,9 @@ const ru: Record = { '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 = { '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 = { '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 diff --git a/client/src/i18n/translations/zh.ts b/client/src/i18n/translations/zh.ts index 1b760893..0ddcf5c9 100644 --- a/client/src/i18n/translations/zh.ts +++ b/client/src/i18n/translations/zh.ts @@ -179,9 +179,6 @@ const zh: Record = { '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 = { 'settings.mcp.endpoint': 'MCP 端点', 'settings.mcp.clientConfig': '客户端配置', 'settings.mcp.clientConfigHint': '将 替换为下方列表中的 API 令牌。npx 的路径可能需要根据您的系统进行调整(例如 Windows 上为 C:\\PROGRA~1\\nodejs\\npx.cmd)。', - 'settings.mcp.clientConfigHintOAuth': 'Replace and 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': '将 替换为上方创建的 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 = { 'settings.mcp.toast.createError': '创建令牌失败', 'settings.mcp.toast.deleted': '令牌已删除', 'settings.mcp.toast.deleteError': '删除令牌失败', + 'settings.mcp.apiTokensDeprecated': 'API 令牌已弃用,将在未来版本中移除。请改用 OAuth 2.1 客户端。', + 'settings.oauth.clients': 'OAuth 2.1 客户端', + 'settings.oauth.clientsHint': '注册 OAuth 2.1 客户端,让第三方 MCP 应用程序(Claude Web、Cursor 等)无需静态令牌即可连接。', + 'settings.oauth.createClient': '新建客户端', + 'settings.oauth.noClients': '没有已注册的 OAuth 客户端。', + 'settings.oauth.clientId': '客户端 ID', + 'settings.oauth.clientSecret': '客户端密钥', + 'settings.oauth.deleteClient': '删除客户端', + 'settings.oauth.deleteClientMessage': '此客户端及所有活跃会话将被永久删除。使用此客户端的任何应用程序将立即失去访问权限。', + 'settings.oauth.rotateSecret': '轮换密钥', + 'settings.oauth.rotateSecretMessage': '将生成新的客户端密钥,所有现有会话将立即失效。在关闭此对话框之前,请更新您的应用程序。', + 'settings.oauth.rotateSecretConfirm': '轮换', + 'settings.oauth.rotateSecretConfirming': '轮换中…', + 'settings.oauth.rotateSecretDoneTitle': '已生成新密钥', + 'settings.oauth.rotateSecretDoneWarning': '此密钥仅显示一次。请立即复制并更新您的应用程序——所有之前的会话已失效。', + 'settings.oauth.activeSessions': '活跃的 OAuth 会话', + 'settings.oauth.sessionScopes': '权限范围', + 'settings.oauth.sessionExpires': '过期时间', + 'settings.oauth.revoke': '撤销', + 'settings.oauth.revokeSession': '撤销会话', + 'settings.oauth.revokeSessionMessage': '这将立即撤销此 OAuth 会话的访问权限。', + 'settings.oauth.modal.createTitle': '注册 OAuth 客户端', + 'settings.oauth.modal.presets': '快速预设', + 'settings.oauth.modal.clientName': '应用程序名称', + 'settings.oauth.modal.clientNamePlaceholder': '例如 Claude Web、我的 MCP 应用', + 'settings.oauth.modal.redirectUris': '重定向 URI', + 'settings.oauth.modal.redirectUrisPlaceholder': 'https://your-app.com/callback\nhttps://your-app.com/auth', + 'settings.oauth.modal.redirectUrisHint': '每行一个 URI。需要 HTTPS(localhost 除外)。要求精确匹配。', + 'settings.oauth.modal.scopes': '允许的权限范围', + 'settings.oauth.modal.scopesHint': 'list_trips 和 get_trip_summary 始终可用——无需权限范围。它们帮助 AI 发现所需的行程 ID。', + 'settings.oauth.modal.selectAll': '全选', + 'settings.oauth.modal.deselectAll': '取消全选', + 'settings.oauth.modal.creating': '注册中…', + 'settings.oauth.modal.create': '注册客户端', + 'settings.oauth.modal.createdTitle': '客户端已注册', + 'settings.oauth.modal.createdWarning': '客户端密钥仅显示一次。请立即复制——无法恢复。', + 'settings.oauth.toast.createError': '注册 OAuth 客户端失败', + 'settings.oauth.toast.deleted': 'OAuth 客户端已删除', + 'settings.oauth.toast.deleteError': '删除 OAuth 客户端失败', + 'settings.oauth.toast.revoked': '会话已撤销', + 'settings.oauth.toast.revokeError': '撤销会话失败', + 'settings.oauth.toast.rotateError': '轮换客户端密钥失败', 'settings.account': '账户', 'settings.about': '关于', 'settings.about.reportBug': '报告错误', @@ -1018,6 +1057,7 @@ const zh: Record = { 'budget.totalBudget': '总预算', 'budget.byCategory': '按分类', 'budget.editTooltip': '点击编辑', + 'budget.linkedToReservation': '已关联到预订——请在那里编辑名称', 'budget.confirm.deleteCategory': '确定删除分类「{name}」及其 {count} 个条目?', 'budget.deleteCategory': '删除分类', 'budget.perPerson': '人均', @@ -1116,6 +1156,9 @@ const zh: Record = { '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 = { '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 = { '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 diff --git a/client/src/i18n/translations/zhTw.ts b/client/src/i18n/translations/zhTw.ts index b156911b..f8e8a22b 100644 --- a/client/src/i18n/translations/zhTw.ts +++ b/client/src/i18n/translations/zhTw.ts @@ -113,6 +113,8 @@ const zhTw: Record = { 'dashboard.tripDescriptionPlaceholder': '這次旅行是關於什麼的?', 'dashboard.startDate': '開始日期', 'dashboard.endDate': '結束日期', + 'dashboard.dayCount': '天數', + 'dashboard.dayCountHint': '未設定旅行日期時,要規劃的天數。', 'dashboard.noDateHint': '未設定日期——將預設建立 7 天。你可以隨時修改。', 'dashboard.coverImage': '封面圖片', 'dashboard.addCoverImage': '新增封面圖片', @@ -127,6 +129,12 @@ const zhTw: Record = { // 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 = { '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 = { '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 = { 'settings.mcp.endpoint': 'MCP 端點', 'settings.mcp.clientConfig': '客戶端配置', 'settings.mcp.clientConfigHint': '將 替換為下方列表中的 API 令牌。npx 的路徑可能需要根據您的系統進行調整(例如 Windows 上為 C:\\PROGRA~1\\nodejs\\npx.cmd)。', - 'settings.mcp.clientConfigHintOAuth': 'Replace and 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': '將 替換為上方建立的 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 = { 'settings.mcp.toast.createError': '建立令牌失敗', 'settings.mcp.toast.deleted': '令牌已刪除', 'settings.mcp.toast.deleteError': '刪除令牌失敗', + 'settings.mcp.apiTokensDeprecated': 'API 金鑰已棄用,將於未來版本中移除。請改用 OAuth 2.1 客戶端。', + 'settings.oauth.clients': 'OAuth 2.1 客戶端', + 'settings.oauth.clientsHint': '註冊 OAuth 2.1 客戶端,讓第三方 MCP 應用程式(Claude Web、Cursor 等)無需靜態金鑰即可連線。', + 'settings.oauth.createClient': '新增客戶端', + 'settings.oauth.noClients': '尚無已註冊的 OAuth 客戶端。', + 'settings.oauth.clientId': '客戶端 ID', + 'settings.oauth.clientSecret': '客戶端密鑰', + 'settings.oauth.deleteClient': '刪除客戶端', + 'settings.oauth.deleteClientMessage': '此客戶端及所有活躍工作階段將被永久刪除。任何使用此客戶端的應用程式將立即失去存取權限。', + 'settings.oauth.rotateSecret': '輪換密鑰', + 'settings.oauth.rotateSecretMessage': '將產生新的客戶端密鑰,所有現有工作階段將立即失效。請在關閉此對話框前更新您的應用程式。', + 'settings.oauth.rotateSecretConfirm': '輪換', + 'settings.oauth.rotateSecretConfirming': '輪換中…', + 'settings.oauth.rotateSecretDoneTitle': '已產生新密鑰', + 'settings.oauth.rotateSecretDoneWarning': '此密鑰僅顯示一次。請立即複製並更新您的應用程式——所有先前的工作階段已失效。', + 'settings.oauth.activeSessions': '活躍的 OAuth 工作階段', + 'settings.oauth.sessionScopes': '授權範圍', + 'settings.oauth.sessionExpires': '到期時間', + 'settings.oauth.revoke': '撤銷', + 'settings.oauth.revokeSession': '撤銷工作階段', + 'settings.oauth.revokeSessionMessage': '這將立即撤銷此 OAuth 工作階段的存取權限。', + 'settings.oauth.modal.createTitle': '註冊 OAuth 客戶端', + 'settings.oauth.modal.presets': '快速預設', + 'settings.oauth.modal.clientName': '應用程式名稱', + 'settings.oauth.modal.clientNamePlaceholder': '例如 Claude Web、我的 MCP 應用程式', + 'settings.oauth.modal.redirectUris': '重新導向 URI', + 'settings.oauth.modal.redirectUrisPlaceholder': 'https://your-app.com/callback\nhttps://your-app.com/auth', + 'settings.oauth.modal.redirectUrisHint': '每行一個 URI。需要 HTTPS(localhost 除外)。需要完全符合。', + 'settings.oauth.modal.scopes': '允許的授權範圍', + 'settings.oauth.modal.scopesHint': 'list_trips 和 get_trip_summary 始終可用——不需要授權範圍。它們可幫助 AI 找到所需的行程 ID。', + 'settings.oauth.modal.selectAll': '全選', + 'settings.oauth.modal.deselectAll': '取消全選', + 'settings.oauth.modal.creating': '註冊中…', + 'settings.oauth.modal.create': '註冊客戶端', + 'settings.oauth.modal.createdTitle': '客戶端已註冊', + 'settings.oauth.modal.createdWarning': '客戶端密鑰僅顯示一次。請立即複製——無法恢復。', + 'settings.oauth.toast.createError': '註冊 OAuth 客戶端失敗', + 'settings.oauth.toast.deleted': 'OAuth 客戶端已刪除', + 'settings.oauth.toast.deleteError': '刪除 OAuth 客戶端失敗', + 'settings.oauth.toast.revoked': '工作階段已撤銷', + 'settings.oauth.toast.revokeError': '撤銷工作階段失敗', + 'settings.oauth.toast.rotateError': '輪換客戶端密鑰失敗', 'settings.account': '賬戶', 'settings.about': '關於', + 'settings.about.reportBug': '回報錯誤', + 'settings.about.reportBugHint': '發現問題?告訴我們', + 'settings.about.featureRequest': '功能建議', + 'settings.about.featureRequestHint': '建議新功能', + 'settings.about.wikiHint': '文件與指南', + 'settings.about.description': 'TREK 是一款自架旅遊規劃器,幫助您從最初構想到最後回憶,整理每次旅行。日程規劃、預算、行李清單、照片及更多功能——全部集中在您自己的伺服器上。', + 'settings.about.madeWith': '以', + 'settings.about.madeBy': '由 Maurice 及不斷成長的開源社群製作。', 'settings.username': '使用者名稱', 'settings.email': '郵箱', 'settings.role': '角色', @@ -386,6 +465,7 @@ const zhTw: Record = { '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 = { '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 = { '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 = { '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 = { 'budget.totalBudget': '總預算', 'budget.byCategory': '按分類', 'budget.editTooltip': '點選編輯', + 'budget.linkedToReservation': '已連結至預訂——請在那裡編輯名稱', 'budget.confirm.deleteCategory': '確定刪除分類「{name}」及其 {count} 個條目?', 'budget.deleteCategory': '刪除分類', 'budget.perPerson': '人均', @@ -1062,6 +1173,7 @@ const zhTw: Record = { 'packing.menuCheckAll': '全部勾選', 'packing.menuUncheckAll': '取消全部勾選', 'packing.menuDeleteCat': '刪除分類', + 'packing.assignUser': '指派使用者', 'packing.addItem': '新增物品', 'packing.addItemPlaceholder': '物品名稱...', 'packing.addCategory': '新增分類', @@ -1070,7 +1182,9 @@ const zhTw: Record = { '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 = { // 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 = { '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 = { '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 = { '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 \ No newline at end of file diff --git a/client/src/pages/AdminPage.test.tsx b/client/src/pages/AdminPage.test.tsx index e4dfad3a..dc0aa6ed 100644 --- a/client/src/pages/AdminPage.test.tsx +++ b/client/src/pages/AdminPage.test.tsx @@ -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(); 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(); - 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(); }); diff --git a/client/src/pages/OAuthAuthorizePage.tsx b/client/src/pages/OAuthAuthorizePage.tsx index 43a114df..457d96ae 100644 --- a/client/src/pages/OAuthAuthorizePage.tsx +++ b/client/src/pages/OAuthAuthorizePage.tsx @@ -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('loading') const [validation, setValidation] = useState(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 = {} 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 {
{groupScopes.map(s => { - const info = SCOPE_GROUPS[s] + const keys = SCOPE_GROUPS[s] return ( ) @@ -304,15 +306,15 @@ export default function OAuthAuthorizePage(): React.ReactElement {

{group}

{groupScopes.map(s => { - const info = SCOPE_GROUPS[s] + const keys = SCOPE_GROUPS[s] return (
{s.endsWith(':delete') ? '🗑️' : s.endsWith(':write') ? '✏️' : '👁️'}
-

{info?.label || s}

-

{info?.description || ''}

+

{keys ? t(keys.labelKey) : s}

+

{keys ? t(keys.descriptionKey) : ''}

) diff --git a/server/src/mcp/index.ts b/server/src/mcp/index.ts index e902ac5d..77407f4e 100644 --- a/server/src/mcp/index.ts +++ b/server/src/mcp/index.ts @@ -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(); +const rateLimitMap = new Map(); -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 | undefined; + if (body?.method !== 'tools/call') return; + const toolName = (body?.params as Record | 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 { 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 { } 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 { 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 { }, }); + 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); diff --git a/server/src/mcp/resources.ts b/server/src/mcp/resources.ts index 7330eeca..87b393dc 100644 --- a/server/src/mcp/resources.ts +++ b/server/src/mcp/resources.ts @@ -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', diff --git a/server/src/mcp/scopes.ts b/server/src/mcp/scopes.ts index f0531456..a77fb433 100644 --- a/server/src/mcp/scopes.ts +++ b/server/src/mcp/scopes.ts @@ -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 = { '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' }, }; // --------------------------------------------------------------------------- diff --git a/server/src/mcp/tools/_shared.ts b/server/src/mcp/tools/_shared.ts index 4978aa74..25bbfe19 100644 --- a/server/src/mcp/tools/_shared.ts +++ b/server/src/mcp/tools/_shared.ts @@ -2,7 +2,7 @@ import { broadcast } from '../../websocket'; export function safeBroadcast(tripId: number, event: string, payload: Record): void { try { - broadcast(tripId, event, payload); + broadcast(tripId, event, { ...payload, _source: 'mcp' }); } catch (err) { console.error(`[MCP] broadcast failed for ${event}:`, err?.message ?? err); } diff --git a/server/src/mcp/tools/assignments.ts b/server/src/mcp/tools/assignments.ts index 6f6e866d..80e6cd6c 100644 --- a/server/src/mcp/tools/assignments.ts +++ b/server/src/mcp/tools/assignments.ts @@ -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 }); diff --git a/server/src/mcp/tools/atlas.ts b/server/src/mcp/tools/atlas.ts index 8fbc9b89..69e4280d 100644 --- a/server/src/mcp/tools/atlas.ts +++ b/server/src/mcp/tools/atlas.ts @@ -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; diff --git a/server/src/mcp/tools/days.ts b/server/src/mcp/tools/days.ts index a15e9c67..adae5cc3 100644 --- a/server/src/mcp/tools/days.ts +++ b/server/src/mcp/tools/days.ts @@ -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 }); diff --git a/server/src/mcp/tools/mapsWeather.ts b/server/src/mcp/tools/mapsWeather.ts index bb55329a..0929d4b6 100644 --- a/server/src/mcp/tools/mapsWeather.ts +++ b/server/src/mcp/tools/mapsWeather.ts @@ -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.', diff --git a/server/src/mcp/tools/tags.ts b/server/src/mcp/tools/tags.ts index 9d27b619..70f4fe8e 100644 --- a/server/src/mcp/tools/tags.ts +++ b/server/src/mcp/tools/tags.ts @@ -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 }); } diff --git a/server/src/mcp/tools/todos.ts b/server/src/mcp/tools/todos.ts index 6861424f..38838c91 100644 --- a/server/src/mcp/tools/todos.ts +++ b/server/src/mcp/tools/todos.ts @@ -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; diff --git a/server/src/mcp/tools/trips.ts b/server/src/mcp/tools/trips.ts index ea65cd61..7712e61b 100644 --- a/server/src/mcp/tools/trips.ts +++ b/server/src/mcp/tools/trips.ts @@ -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) { diff --git a/server/src/routes/admin.ts b/server/src/routes/admin.ts index 870ecdeb..2579dc2d 100644 --- a/server/src/routes/admin.ts +++ b/server/src/routes/admin.ts @@ -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 }); }); diff --git a/server/src/routes/oauth.ts b/server/src/routes/oauth.ts index f76b42cb..1dbe4899 100644 --- a/server/src/routes/oauth.ts +++ b/server/src/routes/oauth.ts @@ -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); diff --git a/server/src/services/oauthService.ts b/server/src/services/oauthService.ts index 6c4a045d..1552aaa8 100644 --- a/server/src/services/oauthService.ts +++ b/server/src/services/oauthService.ts @@ -33,6 +33,7 @@ interface PendingCode { expiresAt: number; } +const MAX_PENDING_CODES = 500; const pendingCodes = new Map(); 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; diff --git a/server/tests/unit/mcp/scopes.test.ts b/server/tests/unit/mcp/scopes.test.ts index df4e7b02..cbeeb38e 100644 --- a/server/tests/unit/mcp/scopes.test.ts +++ b/server/tests/unit/mcp/scopes.test.ts @@ -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', () => { diff --git a/server/tests/unit/mcp/tools-days-accommodations.test.ts b/server/tests/unit/mcp/tools-days-accommodations.test.ts index 5b8780fd..505b6270 100644 --- a/server/tests/unit/mcp/tools-days-accommodations.test.ts +++ b/server/tests/unit/mcp/tools-days-accommodations.test.ts @@ -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(); }); });