diff --git a/frontend/package.json b/frontend/package.json index 69c673603..63abde554 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -94,7 +94,10 @@ "tailwind-merge": "^3.6.0", "tw-animate-css": "^1.4.0", "use-debounce": "^10.1.1", - "zod": "^4.4.3" + "zod": "^4.4.3", + "react-i18next": "^15.5.0", + "i18next": "^25.2.0", + "i18next-browser-languagedetector": "^8.0.2" }, "devDependencies": { "@axe-core/playwright": "^4.12.1", diff --git a/frontend/src/app.tsx b/frontend/src/app.tsx index 1f078b94b..b7484b362 100644 --- a/frontend/src/app.tsx +++ b/frontend/src/app.tsx @@ -1,5 +1,7 @@ import { ApolloProvider } from '@apollo/client/react'; import { lazy, Suspense } from 'react'; +import { I18nextProvider } from 'react-i18next'; +import i18n from './i18n/config'; import { createBrowserRouter, createRoutesFromElements, @@ -277,8 +279,10 @@ function App() { return ( - - + + + + ); diff --git a/frontend/src/components/dashboard/chart-card.tsx b/frontend/src/components/dashboard/chart-card.tsx index f89e1771d..868206929 100644 --- a/frontend/src/components/dashboard/chart-card.tsx +++ b/frontend/src/components/dashboard/chart-card.tsx @@ -6,6 +6,7 @@ import { ResponsiveContainer } from 'recharts'; import { DashboardError } from '@/components/dashboard/dashboard-error'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Spinner } from '@/components/ui/spinner'; +import { useI18n } from '@/hooks/use-i18n'; export function ChartCard({ children, @@ -26,6 +27,8 @@ export function ChartCard({ loading?: boolean; title: ReactNode; }) { + const { t } = useI18n(); + return ( @@ -54,7 +57,7 @@ export function ChartCard({ style={{ height }} > -

No data for this period

+

{t('dashboard.noDataForPeriod')}

) : ( -

Couldn't load

+

{t('errors.couldntLoad')}

); } diff --git a/frontend/src/components/dashboard/metric-card.tsx b/frontend/src/components/dashboard/metric-card.tsx index 687065fd2..f2a7a155d 100644 --- a/frontend/src/components/dashboard/metric-card.tsx +++ b/frontend/src/components/dashboard/metric-card.tsx @@ -4,6 +4,7 @@ import { AlertCircle } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Skeleton } from '@/components/ui/skeleton'; +import { useI18n } from '@/hooks/use-i18n'; export function MetricCard({ className, @@ -22,6 +23,8 @@ export function MetricCard({ title: ReactNode; value: ReactNode; }) { + const { t } = useI18n(); + return ( @@ -48,7 +51,9 @@ export function MetricCard({ (loading ? ( ) : ( -

{error ? "Couldn't load" : description}

+

+ {error ? t('errors.couldntLoad') : description} +

))}
diff --git a/frontend/src/components/layouts/main/main-sidebar.tsx b/frontend/src/components/layouts/main/main-sidebar.tsx index d917965be..71eb506d2 100644 --- a/frontend/src/components/layouts/main/main-sidebar.tsx +++ b/frontend/src/components/layouts/main/main-sidebar.tsx @@ -18,6 +18,7 @@ import { UserIcon, } from 'lucide-react'; import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import { Link, useLocation, useMatch, useParams } from 'react-router-dom'; import type { Flow } from '@/providers/sidebar-flows-provider'; @@ -62,6 +63,7 @@ interface FlowMenuItemProps { } export function MainSidebar() { + const { t } = useTranslation(); const location = useLocation(); const isDashboardActive = useMatch('/dashboard'); const isFlowsActive = useMatch('/flows/*'); @@ -107,7 +109,7 @@ export function MainSidebar() {
- PentAGI + {t('sidebar.appName')}
@@ -120,7 +122,7 @@ export function MainSidebar() { - New Flow + {t('sidebar.newFlow')} @@ -131,7 +133,7 @@ export function MainSidebar() { > - Dashboard + {t('sidebar.dashboard')} @@ -142,7 +144,7 @@ export function MainSidebar() { > - Flows + {t('sidebar.flows')} @@ -165,7 +167,7 @@ export function MainSidebar() { > - Templates + {t('sidebar.templates')} @@ -188,14 +190,14 @@ export function MainSidebar() { > - Resources + {t('sidebar.resources')} @@ -208,7 +210,7 @@ export function MainSidebar() { > - Knowledges + {t('sidebar.knowledges')} @@ -232,7 +234,7 @@ export function MainSidebar() { - Recent Flows + {t('sidebar.recentFlows')} @@ -254,7 +256,7 @@ export function MainSidebar() { - Favorite Flows + {t('sidebar.favoriteFlows')} @@ -284,7 +286,7 @@ export function MainSidebar() { to={routes.settings.root} > - Settings + {t('sidebar.settings')} @@ -335,7 +337,7 @@ export function MainSidebar() { onSelect={(event) => event.preventDefault()} > - Theme + {t('sidebar.theme')} setTheme(value as Theme)} @@ -343,21 +345,21 @@ export function MainSidebar() { > @@ -373,13 +375,13 @@ export function MainSidebar() { to={routes.settings.account} > - Profile + {t('sidebar.profile')} logout()}> - Log out + {t('sidebar.logout')} @@ -403,6 +405,8 @@ export function MainSidebar() { } function FlowMenuItem({ activeFlowId, flow, isFavorite, onToggleFavorite }: FlowMenuItemProps) { + const { t } = useTranslation(); + return ( onToggleFavorite(flow.id)} diff --git a/frontend/src/components/layouts/settings/settings-sidebar.tsx b/frontend/src/components/layouts/settings/settings-sidebar.tsx index 1ea570f22..9fbcb2454 100644 --- a/frontend/src/components/layouts/settings/settings-sidebar.tsx +++ b/frontend/src/components/layouts/settings/settings-sidebar.tsx @@ -2,6 +2,7 @@ import type { ReactNode } from 'react'; import { ArrowLeft, FileText, Key, Plug, Settings as SettingsIcon, User } from 'lucide-react'; import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { NavLink, useLocation } from 'react-router-dom'; import { @@ -22,7 +23,7 @@ interface MenuItem { icon?: ReactNode; id: string; path: string; - title: string; + titleKey: string; } interface SettingsSidebarMenuItemProps { @@ -34,29 +35,30 @@ const menuItems: readonly MenuItem[] = [ icon: , id: 'account', path: routes.settings.account, - title: 'Account', + titleKey: 'settings.account', }, { icon: , id: 'providers', path: routes.settings.providers, - title: 'Providers', + titleKey: 'settings.providers', }, { icon: , id: 'prompts', path: routes.settings.prompts, - title: 'Prompts', + titleKey: 'settings.prompts', }, { icon: , id: 'api-tokens', path: routes.settings.apiTokens, - title: 'API Tokens', + titleKey: 'settings.apiTokens', }, ] as const; export function SettingsSidebar() { + const { t } = useTranslation(); const location = useLocation(); const [returnUrl] = useState(() => getSafeReturnUrl((location.state as null | { from?: string })?.from ?? null, routes.flows), @@ -71,7 +73,7 @@ export function SettingsSidebar() {
- Settings + {t('settings.settingsTitle')}
@@ -94,7 +96,7 @@ export function SettingsSidebar() { - Back to App + {t('settings.backToApp')} @@ -103,6 +105,7 @@ export function SettingsSidebar() { } function SettingsSidebarMenuItem({ item }: SettingsSidebarMenuItemProps) { + const { t } = useTranslation(); const location = useLocation(); const isActive = location.pathname.startsWith(item.path); @@ -114,7 +117,7 @@ function SettingsSidebarMenuItem({ item }: SettingsSidebarMenuItemProps) { > {item.icon} - {item.title} + {t(item.titleKey)} diff --git a/frontend/src/components/shared/detail-navigation/detail-navigation-buttons.tsx b/frontend/src/components/shared/detail-navigation/detail-navigation-buttons.tsx index 48bd09725..6c2d9f5bf 100644 --- a/frontend/src/components/shared/detail-navigation/detail-navigation-buttons.tsx +++ b/frontend/src/components/shared/detail-navigation/detail-navigation-buttons.tsx @@ -2,6 +2,7 @@ import { ChevronLeft, ChevronRight } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { useI18n } from '@/hooks/use-i18n'; import { cn } from '@/lib/utils'; import type { DetailNavigationController } from './use-detail-navigation'; @@ -32,6 +33,7 @@ export function DetailNavigationButtons({ sheetTitle, size = 'default', }: DetailNavigationButtonsProps) { + const { t } = useI18n(); const lowerTitle = sheetTitle.toLowerCase(); const isSm = size === 'sm'; const sideButtonSize = isSm ? 'size-7' : 'size-8'; @@ -43,7 +45,7 @@ export function DetailNavigationButtons({ - Previous + {t('detailNavigation.previous')} - Show all matching {lowerTitle} + {t('detailNavigation.showAllMatching', { title: lowerTitle })} - Next + {t('detailNavigation.next')} ); diff --git a/frontend/src/components/shared/detail-navigation/detail-navigation-sheet.tsx b/frontend/src/components/shared/detail-navigation/detail-navigation-sheet.tsx index 062a51528..2a6fac6b2 100644 --- a/frontend/src/components/shared/detail-navigation/detail-navigation-sheet.tsx +++ b/frontend/src/components/shared/detail-navigation/detail-navigation-sheet.tsx @@ -5,6 +5,7 @@ import { Badge } from '@/components/ui/badge'; import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from '@/components/ui/input-group'; import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'; import { useElementVirtualList } from '@/hooks/use-element-virtual-list'; +import { useI18n } from '@/hooks/use-i18n'; import { cn } from '@/lib/utils'; import type { DetailNavigationController } from './use-detail-navigation'; @@ -43,10 +44,12 @@ export function DetailNavigationSheet({ controller, hasSearch = true, renderItem, - searchPlaceholder = 'Search…', + searchPlaceholder, sheetIcon, sheetTitle, }: DetailNavigationSheetProps) { + const { t } = useI18n(); + const resolvedSearchPlaceholder = searchPlaceholder ?? t('detailNavigation.searchPlaceholder'); const { clearSearchQuery, currentId, @@ -395,11 +398,11 @@ export function DetailNavigationSheet({ /> ({ {hasClearButton ? ( ({ ) : (
{trimmedQuery.length > 0 - ? `No items match "${trimmedQuery}".` - : 'No items match the current filter.'} + ? t('detailNavigation.noItemsMatchQuery', { query: trimmedQuery }) + : t('detailNavigation.noItemsMatchFilter')}
)} diff --git a/frontend/src/components/shared/file-manager/file-manager-actions.tsx b/frontend/src/components/shared/file-manager/file-manager-actions.tsx index 7dc17dca2..b1534534f 100644 --- a/frontend/src/components/shared/file-manager/file-manager-actions.tsx +++ b/frontend/src/components/shared/file-manager/file-manager-actions.tsx @@ -1,5 +1,7 @@ import { ClipboardCopy, Copy, Download, FileSymlink, FolderOutput, Trash2 } from 'lucide-react'; +import i18n from '@/i18n/config'; + import type { FileManagerAction, FileManagerBulkAction, FileNode } from './file-manager-types'; /** @@ -21,7 +23,7 @@ export const downloadAction = ( getHrefDownloadAttr: (file) => (file.isDir ? `${file.name}.${archiveExtension}` : file.name), icon: Download, id: '__builtin_download', - label: 'Download', + label: i18n.t('fileManager.download'), onSelect: () => {}, }; }; @@ -30,7 +32,7 @@ export const copyPathAction = (onCopyPath: (file: FileNode) => void): FileManage appliesToDirs: true, icon: ClipboardCopy, id: '__builtin_copy_path', - label: 'Copy path', + label: i18n.t('fileManager.copyPath'), onSelect: onCopyPath, }); @@ -42,7 +44,7 @@ export const deleteAction = (onDelete: (file: FileNode) => void): FileManagerAct appliesToDirs: true, icon: Trash2, id: '__builtin_delete', - label: 'Delete', + label: i18n.t('fileManager.delete'), onSelect: onDelete, separatorBefore: true, variant: 'destructive', @@ -68,15 +70,15 @@ export const bulkDeleteAction = ( onDelete: (files: FileNode[]) => Promise | void, options: BulkDeleteOptions = {}, ): FileManagerBulkAction => { - const label = options.label ?? 'Delete'; + const label = options.label ?? i18n.t('fileManager.delete'); return { confirm: { confirmText: options.confirmText ?? label, description: options.confirmDescription ?? - ((countLabel) => `This will delete ${countLabel}. This action cannot be undone.`), - title: options.confirmTitle ?? ((countLabel) => `Delete ${countLabel}`), + ((countLabel) => i18n.t('fileManager.deleteConfirmDescription', { count: countLabel })), + title: options.confirmTitle ?? ((countLabel) => i18n.t('fileManager.deleteConfirmTitle', { count: countLabel })), }, icon: Trash2, id: '__builtin_bulk_delete', @@ -97,7 +99,7 @@ export const bulkCopyPathsAction = ( ): FileManagerBulkAction => ({ icon: ClipboardCopy, id: '__builtin_bulk_copy_paths', - label: options.label ?? 'Copy paths', + label: options.label ?? i18n.t('fileManager.copyPaths'), onSelect: (files) => onCopy(files.map((file) => file.path)), overflow: options.overflow ?? true, }); @@ -112,7 +114,7 @@ export const bulkMoveAction = ( ): FileManagerBulkAction => ({ icon: FileSymlink, id: '__builtin_bulk_move', - label: options.label ?? 'Move to…', + label: options.label ?? i18n.t('fileManager.moveTo'), onSelect: onMove, overflow: options.overflow, }); @@ -127,7 +129,7 @@ export const bulkCopyAction = ( ): FileManagerBulkAction => ({ icon: Copy, id: '__builtin_bulk_copy', - label: options.label ?? 'Copy to…', + label: options.label ?? i18n.t('fileManager.copyTo'), onSelect: onCopy, overflow: options.overflow, }); @@ -142,7 +144,7 @@ export const bulkPromoteAction = ( ): FileManagerBulkAction => ({ icon: FolderOutput, id: '__builtin_bulk_promote', - label: options.label ?? 'Save as resources', + label: options.label ?? i18n.t('fileManager.saveAsResources'), onSelect: onPromote, overflow: options.overflow, }); @@ -186,7 +188,7 @@ export const bulkDownloadAction = ( ): FileManagerBulkAction => ({ icon: Download, id: '__builtin_bulk_download', - label: options.label ?? 'Download', + label: options.label ?? i18n.t('fileManager.download'), onSelect: (files) => { if (files.length === 0) { return; diff --git a/frontend/src/components/shared/file-manager/file-manager-bulk-actions-bar.tsx b/frontend/src/components/shared/file-manager/file-manager-bulk-actions-bar.tsx index 319e9b722..6ebd0ffa9 100644 --- a/frontend/src/components/shared/file-manager/file-manager-bulk-actions-bar.tsx +++ b/frontend/src/components/shared/file-manager/file-manager-bulk-actions-bar.tsx @@ -3,6 +3,7 @@ import { type ComponentType, useCallback, useMemo, useState } from 'react'; import ConfirmationDialog from '@/components/shared/confirmation-dialog'; import { Button } from '@/components/ui/button'; +import { useI18n } from '@/hooks/use-i18n'; import { DropdownMenu, DropdownMenuContent, @@ -58,6 +59,7 @@ export function FileManagerBulkActionsBar({ selectedPaths, selectionTotalBytes, }: FileManagerBulkActionsBarProps) { + const { t } = useI18n(); const [pendingAction, setPendingAction] = useState(null); const dedupedFiles = useMemo(() => { @@ -128,11 +130,12 @@ export function FileManagerBulkActionsBar({ const pluralize = labels.pluralizeItems ?? pluralizeItemsEnglish; const countLabel = pluralize(selectedPaths.size); - const baseSelectedText = labels.selectedLabel?.(selectedPaths.size) ?? `${selectedPaths.size} selected`; + const baseSelectedText = + labels.selectedLabel?.(selectedPaths.size) ?? t('fileManager.selectedCount', { count: selectedPaths.size }); const sizeSuffix = (labels.formatSelectionSize ?? formatFileSize)(selectionTotalBytes); const selectedText = sizeSuffix ? `${baseSelectedText} · ${sizeSuffix}` : baseSelectedText; - const cancelText = labels.bulkCancel ?? 'Cancel'; - const moreActionsText = labels.bulkMoreActions ?? 'More actions'; + const cancelText = labels.bulkCancel ?? t('fileManager.cancel'); + const moreActionsText = labels.bulkMoreActions ?? t('fileManager.moreActions'); return ( <> diff --git a/frontend/src/components/shared/file-manager/file-manager-row.tsx b/frontend/src/components/shared/file-manager/file-manager-row.tsx index 9a4f1009e..27c4c8dd9 100644 --- a/frontend/src/components/shared/file-manager/file-manager-row.tsx +++ b/frontend/src/components/shared/file-manager/file-manager-row.tsx @@ -28,6 +28,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; +import { useI18n } from '@/hooks/use-i18n'; import { cn } from '@/lib/utils'; import type { FileManagerAction, FileManagerInternalNode, FileNode } from './file-manager-types'; @@ -187,6 +188,7 @@ function FileManagerRowImpl({ posInSet, setSize, }: FileManagerRowProps) { + const { t } = useI18n(); const { formatModified = defaultFormatModified, gridTemplate, @@ -439,7 +441,7 @@ function FileManagerRowImpl({ {...skipRowClickProps} > diff --git a/frontend/src/components/shared/unsaved-changes/unsaved-changes-dialog.tsx b/frontend/src/components/shared/unsaved-changes/unsaved-changes-dialog.tsx index 472d0b313..82f8c11ba 100644 --- a/frontend/src/components/shared/unsaved-changes/unsaved-changes-dialog.tsx +++ b/frontend/src/components/shared/unsaved-changes/unsaved-changes-dialog.tsx @@ -12,6 +12,7 @@ import { DialogTitle, } from '@/components/ui/dialog'; import { Spinner } from '@/components/ui/spinner'; +import { useI18n } from '@/hooks/use-i18n'; export interface UnsavedChangesDialogProps { /** When `false`, the "Save & leave" button is disabled (e.g. form is invalid). */ @@ -32,8 +33,8 @@ export interface UnsavedChangesDialogProps { function UnsavedChangesDialog({ canSave, - description = 'You have unsaved changes on this page. Would you like to save them before leaving?', - discardText = 'Discard', + description, + discardText, handleCancel, handleDiscard, handleOpenChange, @@ -41,9 +42,15 @@ function UnsavedChangesDialog({ isOpen, isSavingFromDialog, saveIcon = , - saveText = 'Save', - title = 'Unsaved changes', + saveText, + title, }: UnsavedChangesDialogProps) { + const { t } = useI18n(); + const resolvedDescription = description ?? t('unsavedChanges.description'); + const resolvedDiscardText = discardText ?? t('unsavedChanges.discardChanges'); + const resolvedSaveText = saveText ?? t('common.save'); + const resolvedTitle = title ?? t('unsavedChanges.title'); + return ( - {title} - {description} + {resolvedTitle} + {resolvedDescription} diff --git a/frontend/src/components/ui/breadcrumb.tsx b/frontend/src/components/ui/breadcrumb.tsx index a06378bc3..8dcf8c29a 100644 --- a/frontend/src/components/ui/breadcrumb.tsx +++ b/frontend/src/components/ui/breadcrumb.tsx @@ -2,6 +2,7 @@ import { Slot } from '@radix-ui/react-slot'; import { ChevronRight, MoreHorizontal } from 'lucide-react'; import * as React from 'react'; +import { useI18n } from '@/hooks/use-i18n'; import { cn } from '@/lib/utils'; function Breadcrumb({ @@ -19,6 +20,8 @@ function Breadcrumb({ } function BreadcrumbEllipsis({ className, ...props }: React.ComponentProps<'span'>) { + const { t } = useI18n(); + return ( ); } diff --git a/frontend/src/components/ui/data-table.tsx b/frontend/src/components/ui/data-table.tsx index ca4c66548..676fcf272 100644 --- a/frontend/src/components/ui/data-table.tsx +++ b/frontend/src/components/ui/data-table.tsx @@ -42,6 +42,8 @@ import { import { useLocation } from 'react-router-dom'; import { useDebouncedCallback } from 'use-debounce'; +import { useI18n } from '@/hooks/use-i18n'; + import { Button } from '@/components/ui/button'; import { ContextMenu, ContextMenuContent, ContextMenuTrigger } from '@/components/ui/context-menu'; import { @@ -193,8 +195,10 @@ interface DataTableFilterProps { } function DataTableEmptyState({ entityName, filterValue }: DataTableEmptyStateProps) { + const { t } = useI18n(); + if (!entityName) { - return <>No results.; + return <>{t('common.noResults')}; } const hasFilter = filterValue.length > 0; @@ -206,10 +210,13 @@ function DataTableEmptyState({ entityName, filterValue }: DataTableEmptyStatePro - {hasFilter ? 'No matches' : `No ${entityName} yet`} + + {hasFilter ? t('common.noMatches') : t('common.noEntityYet', { entity: entityName })} + {hasFilter ? ( - No {entityName} match {filterValue}. Try a different query. + {t('common.noEntityMatchPrefix', { entity: entityName })} {filterValue} + {t('common.noEntityMatchSuffix')} ) : null} @@ -310,7 +317,7 @@ function DataTable({ data, empty, filterColumn, - filterPlaceholder = 'Filter...', + filterPlaceholder, filterValue: externalFilterValue, initialPageSize = 10, initialSorting = [], @@ -329,6 +336,7 @@ function DataTable({ const isFilterControlled = externalFilterValue !== undefined && onFilterChange !== undefined; const isRowInteractive = !!onRowClick || !!renderSubComponent; + const { t } = useI18n(); const { pathname } = useLocation(); // When `storageKey` is passed by the parent it wins — multi-table routes // (e.g. /settings/prompts) need distinct slots per instance, otherwise @@ -688,7 +696,7 @@ function DataTable({ {searchCandidateIds.length > 0 ? ( table.setGlobalFilter(value)} - placeholder={filterPlaceholder} + placeholder={filterPlaceholder ?? t('common.filterEllipsis')} query={effectiveQuery} /> ) : null} @@ -696,7 +704,7 @@ function DataTable({ ); } diff --git a/frontend/src/hooks/use-i18n.ts b/frontend/src/hooks/use-i18n.ts new file mode 100644 index 000000000..55191debe --- /dev/null +++ b/frontend/src/hooks/use-i18n.ts @@ -0,0 +1,26 @@ +import { useTranslation } from 'react-i18next'; + +export const useI18n = () => { + const { t, i18n } = useTranslation(); + + const changeLanguage = (lng: string) => { + i18n.changeLanguage(lng); + // Store in localStorage for persistence + localStorage.setItem('i18nextLng', lng); + }; + + const currentLanguage = i18n.language || 'en'; + + return { + t, + changeLanguage, + currentLanguage, + i18n, + }; +}; + +// Convenience hook for common translations +export const useTranslations = () => { + const { t } = useTranslation(); + return t; +}; \ No newline at end of file diff --git a/frontend/src/i18n/config.ts b/frontend/src/i18n/config.ts new file mode 100644 index 000000000..0faa8d26b --- /dev/null +++ b/frontend/src/i18n/config.ts @@ -0,0 +1,42 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; + +// Import translations +import enTranslations from '../locales/en.json'; +import trTranslations from '../locales/tr.json'; + +// the translations +const resources = { + en: { + translation: enTranslations, + }, + tr: { + translation: trTranslations, + }, +}; + +i18n + // detect user language + .use(LanguageDetector) + // pass the i18n instance to react-i18next + .use(initReactI18next) + // init i18next + .init({ + resources, + lng: localStorage.getItem('i18nextLng') ?? 'tr', + fallbackLng: 'tr', + supportedLngs: ['en', 'tr'], + detection: { + order: ['localStorage', 'navigator'], + caches: ['localStorage'], + }, + interpolation: { + escapeValue: false, // React already escapes values + }, + react: { + useSuspense: false, // no suspense for now + }, + }); + +export default i18n; \ No newline at end of file diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json new file mode 100644 index 000000000..35eebe5d0 --- /dev/null +++ b/frontend/src/locales/en.json @@ -0,0 +1,866 @@ +{ + "common": { + "dashboard": "Dashboard", + "flows": "Flows", + "templates": "Templates", + "resources": "Resources", + "knowledges": "Knowledges", + "settings": "Settings", + "profile": "Profile", + "logout": "Log out", + "newFlow": "New Flow", + "recentFlows": "Recent Flows", + "favoriteFlows": "Favorite Flows", + "theme": "Theme", + "edit": "Edit", + "delete": "Delete", + "save": "Save", + "cancel": "Cancel", + "confirm": "Confirm", + "close": "Close", + "search": "Search", + "filter": "Filter", + "loading": "Loading...", + "error": "Error", + "success": "Success", + "warning": "Warning", + "info": "Info", + "yes": "Yes", + "no": "No", + "back": "Back", + "next": "Next", + "previous": "Previous", + "create": "Create", + "update": "Update", + "view": "View", + "details": "Details", + "actions": "Actions", + "status": "Status", + "title": "Title", + "description": "Description", + "name": "Name", + "email": "Email", + "password": "Password", + "language": "Language", + "english": "English", + "turkish": "Türkçe", + "selectLanguage": "Select Language", + "rename": "Rename", + "clone": "Clone", + "reset": "Reset", + "finish": "Finish", + "test": "Test", + "validate": "Validate", + "diff": "Diff", + "reload": "Reload", + "tryAgain": "Try again", + "notAvailable": "N/A", + "openMenu": "Open menu", + "custom": "Custom", + "default": "Default", + "add": "Add", + "remove": "Remove", + "or": "or", + "searchIn": "Search in", + "columns": "Columns", + "rowsPerPage": "Rows per page", + "firstPage": "First page", + "previousPage": "Previous page", + "nextPage": "Next page", + "lastPage": "Last page", + "clearSearch": "Clear search", + "allOption": "All", + "filterEllipsis": "Filter...", + "pageOf": "Page {{page}} of {{pageCount}}", + "noResults": "No results.", + "noResultsPlain": "No results", + "noMatches": "No matches", + "noEntityYet": "No {{entity}} yet", + "noEntityMatchPrefix": "No {{entity}} match", + "noEntityMatchSuffix": ". Try a different query.", + "showingRange": "Showing {{start}}–{{end}} of {{total}}", + "noEntity": "No {{entity}}", + "toggleSidebar": "Toggle Sidebar", + "dismissDialog": "Dismiss dialog", + "dismissSheet": "Dismiss sheet", + "sidebar": "Sidebar", + "more": "More", + "displaysMobileSidebar": "Displays the mobile sidebar." + }, + "sidebar": { + "appName": "PentAGI", + "newFlow": "New Flow", + "dashboard": "Dashboard", + "flows": "Flows", + "templates": "Templates", + "resources": "Resources", + "knowledges": "Knowledges", + "recentFlows": "Recent Flows", + "favoriteFlows": "Favorite Flows", + "settings": "Settings", + "profile": "Profile", + "logout": "Log out", + "theme": "Theme", + "newFlowAction": "New flow", + "newTemplateAction": "New template", + "newKnowledgeAction": "New knowledge", + "uploadFile": "Upload file", + "systemTheme": "System theme", + "lightTheme": "Light theme", + "darkTheme": "Dark theme", + "toggleFavorite": "Toggle favorite" + }, + "flow": { + "createNewFlow": "Create New Flow", + "flowTitle": "Flow Title", + "flowDescription": "Flow Description", + "flowStatus": "Flow Status", + "running": "Running", + "completed": "Completed", + "failed": "Failed", + "pending": "Pending", + "created": "Created", + "waiting": "Waiting", + "finished": "Finished", + "startFlow": "Start Flow", + "stopFlow": "Stop Flow", + "restartFlow": "Restart Flow", + "deleteFlow": "Delete Flow", + "editFlow": "Edit Flow", + "viewDetails": "View Details", + "newFlow": "New flow", + "createFlow": "Create a new flow", + "describeFlow": "Describe what you would like PentAGI to test", + "automation": "Automation", + "assistant": "Assistant", + "noFlowsFound": "No flows found", + "getStarted": "Get started by creating your first conversation flow", + "loadingFlows": "Loading flows...", + "loadingFlowsDesc": "Please wait while we fetch your conversation flows", + "errorLoadingFlows": "Error loading flows", + "filterFlows": "Filter flows...", + "id": "ID", + "provider": "Provider", + "terminals": "Terminals", + "noTerminals": "No terminals", + "renameSuccess": "Flow renamed successfully", + "renameFailed": "Failed to rename flow", + "addToFavorites": "Add to favorites", + "removeFromFavorites": "Remove from favorites", + "doubleClickRename": "Double-click to rename", + "selectFlow": "Select a flow", + "report": "Report", + "openWebView": "Open web view", + "copyToClipboard": "Copy to clipboard", + "downloadMD": "Download MD", + "downloadPDF": "Download PDF", + "reportCopied": "Report copied to clipboard", + "reportCopyFailed": "Failed to copy report to clipboard", + "errorLoadingFlow": "Error loading flow", + "creatingFlow": "Creating a new flow...", + "describeAutomation": "Describe what you would like PentAGI to test...", + "describeAssistant": "What would you like me to help you with?", + "finishing": "Finishing...", + "deleting": "Deleting...", + "renaming": "Renaming", + "toggleFavorite": "Toggle favorite", + "openMenu": "Open menu", + "flowActions": "Flow actions", + "connected": "connected", + "disconnected": "disconnected", + "updatedAt": "Updated", + "itemType": "flow", + "flowNumber": "Flow #{{id}}", + "loadingReport": "Loading Report...", + "generatingPdf": "Generating PDF...", + "preparingReportDesc": "Please wait while we prepare your penetration testing report.", + "creatingPdfDesc": "Creating your PDF document. This may take a few moments.", + "errorLoadingReport": "Error Loading Report", + "failedToLoadFlowData": "Failed to load flow data", + "unexpectedReportError": "An unexpected error occurred while loading the report.", + "failedToGeneratePdf": "Failed to generate PDF", + "flowBreadcrumb": "Flow" + }, + "auth": { + "login": "Login", + "signup": "Sign Up", + "forgotPassword": "Forgot Password?", + "rememberMe": "Remember Me", + "username": "Username", + "confirmPassword": "Confirm Password", + "loginSuccess": "Login successful!", + "logoutSuccess": "Logout successful!", + "signIn": "Sign in", + "updatePassword": "Update Password", + "changePasswordRequired": "You need to change your password before continuing.", + "loginLabel": "Login", + "enterEmail": "Enter your email", + "enterPassword": "Enter your password", + "invalidLogin": "Invalid login or password", + "authFailed": "Authentication failed", + "continueWithGoogle": "Continue with Google", + "continueWithGithub": "Continue with GitHub", + "authInProgress": "Authentication in progress...", + "authCompleteClosing": "Authentication complete, closing window...", + "errorCommunicatingWithParent": "Error communicating with parent window. Closing in a few seconds...", + "authWindowOpenedDirectly": "Authentication window opened directly. Redirecting to login page..." + }, + "settings": { + "general": "General", + "account": "Account", + "appearance": "Appearance", + "languageSettings": "Language Settings", + "notifications": "Notifications", + "privacy": "Privacy", + "apiTokens": "API Tokens", + "providers": "Providers", + "prompts": "Prompts", + "darkMode": "Dark Mode", + "lightMode": "Light Mode", + "system": "System", + "saveChanges": "Save Changes", + "backToApp": "Back to App", + "settingsTitle": "Settings" + }, + "account": { + "title": "Account", + "displayName": "Display name", + "displayNameDesc": "The name shown across the app.", + "emailAddress": "Email address", + "emailLinkedFrom": "Linked from your {{provider}}.", + "emailSignIn": "The email you use to sign in.", + "password": "Password", + "changePassword": "Change your account password.", + "change": "Change", + "memberSince": "Member since {{date}}", + "localAccount": "Local account", + "oauthAccount": "OAuth account", + "nameRequired": "Name is required", + "nameMaxLength": "Name must not exceed 70 characters", + "nameUpdated": "Name successfully updated", + "nameUpdateFailed": "Failed to update name", + "enterDisplayName": "Enter your display name", + "updateName": "Update Name", + "currentPassword": "Current Password", + "newPassword": "New Password", + "confirmNewPassword": "Confirm New Password", + "enterCurrentPassword": "Enter your current password", + "enterNewPassword": "Enter your new password", + "confirmNewPasswordPlaceholder": "Confirm your new password", + "updatePasswordButton": "Update Password", + "passwordUpdated": "Password successfully changed", + "passwordUpdateFailed": "Failed to change password", + "passwordMinLength": "Password must be at least 8 characters", + "passwordMaxLength": "Password must not exceed 72 characters", + "passwordComplexity": "Password must be either longer than 15 characters, or at least 8 characters with a number, lowercase, uppercase, and special character (!@#$&*)", + "passwordsDontMatch": "Passwords don't match", + "passwordSameAsCurrent": "New password must be different from current password", + "passwordHint": "Must be 16+ characters, or 8+ with number, lowercase, uppercase, and special character (!@#$&*)", + "skipForNow": "Skip for now", + "newEmail": "New Email", + "enterNewEmail": "Enter your new email address", + "emailRequired": "Email is required", + "emailInvalid": "Invalid email address", + "emailMaxLength": "Email must not exceed 50 characters", + "emailUpdated": "Email successfully updated", + "emailUpdateFailed": "Failed to update email", + "currentPasswordRequired": "Current password is required", + "updateEmail": "Update Email" + }, + "apiTokens": { + "title": "API Tokens", + "createToken": "Create Token", + "tokenName": "Name", + "tokenId": "Token ID", + "status": "Status", + "expires": "Expires", + "createdAt": "Created", + "active": "active", + "revoked": "revoked", + "expired": "expired", + "unnamed": "(unnamed)", + "tokenNamePlaceholder": "Token name (optional)", + "pickDate": "Pick date", + "copyTokenId": "Copy token ID", + "openMenu": "Open menu", + "filterTokens": "Filter tokens...", + "noTokensTitle": "No API tokens configured", + "noTokensDesc": "Create your first API token to access PentAGI programmatically", + "loadingTokens": "Loading tokens...", + "loadingTokensDesc": "Please wait while we fetch your API tokens", + "errorLoadingTokens": "Error loading tokens", + "createTokenTitle": "API Token Created", + "createTokenDesc": "Copy this token now. You won't be able to see it again for security reasons.", + "copyToken": "Copy Token", + "tokenCopied": "Token copied to clipboard", + "tokenCopyFailed": "Failed to copy token to clipboard", + "tokenIdCopied": "Token ID copied to clipboard", + "tokenIdCopyFailed": "Failed to copy token ID to clipboard", + "createTokenFailed": "Failed to create token", + "updateTokenFailed": "Failed to update token", + "deleteTokenFailed": "Failed to delete token", + "graphqlPlayground": "GraphQL Playground", + "swaggerUi": "Swagger UI", + "developerTools": "Developer tools", + "tokenNameMax": "Token name must be 100 characters or less", + "expirationRequired": "Expiration date is required", + "submitting": "Submitting…", + "submit": "Submit", + "deleteToken": "Delete token", + "deleting": "Deleting...", + "tokenNoun": "token" + }, + "providers": { + "title": "Providers", + "createProvider": "Create Provider", + "editProvider": "Edit Provider", + "noProvidersTitle": "No providers configured", + "noProvidersDesc": "Get started by adding your first language model provider", + "addProvider": "Add Provider", + "loadingProviders": "Loading providers...", + "loadingProvidersDesc": "Please wait while we fetch your provider configurations", + "errorLoadingProviders": "Error loading providers", + "filterProviders": "Filter providers...", + "noAvailableTypes": "No available provider types", + "name": "Name", + "type": "Type", + "createdAt": "Created", + "updatedAt": "Updated", + "agentConfigurations": "Agent Configurations", + "noAgentConfig": "No agent configuration available", + "noConfig": "No configuration available", + "deleteFailed": "Failed to delete provider", + "deleteProviderTitle": "Delete provider", + "cloneProvider": "Clone", + "providerTypeRequired": "Provider type is required", + "providerNameRequired": "Provider name is required", + "providerNameMax": "Maximum 50 characters allowed", + "modelRequired": "Model is required", + "minLengthExceed": "Min length must not exceed max length", + "maxReasoningTokens": "Maximum 32000 tokens", + "validJsonObject": "Must be a valid JSON object", + "createNewProvider": "Create a new provider", + "editProviderTitle": "Edit provider", + "configureNewProvider": "Configure a new language model provider", + "updateProviderSettings": "Update provider settings and configuration", + "providerType": "Type", + "providerTypeDesc": "The type of language model provider", + "providerName": "Name", + "providerNameDesc": "A unique name for your provider configuration", + "selectProvider": "Select provider", + "enterProviderName": "Enter provider name", + "loadingProviderData": "Loading provider data...", + "loadingProviderDataDesc": "Please wait while we fetch provider configuration", + "errorLoadingProviderData": "Error loading provider data", + "testProvider": "Test", + "testing": "Testing...", + "testResults": "Provider Test Results", + "passed": "passed", + "model": "Model", + "temperature": "Temperature", + "maxTokens": "Max Tokens", + "topP": "Top P", + "topK": "Top K", + "minLength": "Min Length", + "maxLength": "Max Length", + "repetitionPenalty": "Repetition Penalty", + "frequencyPenalty": "Frequency Penalty", + "presencePenalty": "Presence Penalty", + "reasoningConfig": "Reasoning Configuration", + "reasoningMode": "Reasoning Mode", + "reasoningEffort": "Reasoning Effort", + "reasoningMaxTokens": "Reasoning Max Tokens", + "priceConfig": "Price Configuration", + "inputPrice": "Input Price", + "outputPrice": "Output Price", + "cacheReadPrice": "Cache Read Price", + "cacheWritePrice": "Cache Write Price", + "inputPriceDesc": "Price per 1M input tokens", + "outputPriceDesc": "Price per 1M output tokens", + "cacheReadPriceDesc": "Price per 1M cached read tokens", + "cacheWritePriceDesc": "Price per 1M cache write tokens", + "extraBody": "Extra Body", + "extraBodyTitle": "Extra Body", + "extraBodyDesc": "Provider-specific request body fields as a JSON object, merged into every call (e.g. vLLM chat_template_kwargs).", + "extraBodyLabel": "Extra Body (JSON)", + "testResultSuccess": "Success", + "testResultFailed": "Failed", + "testResultUnknown": "Unknown", + "reasoning": "Reasoning", + "streaming": "Streaming", + "noTestsAvailable": "No tests available for this agent", + "notSelected": "Not selected", + "adaptive": "Adaptive", + "budget": "Budget", + "off": "Off (no thinking)", + "adaptiveThinkingDesc": "This model thinks adaptively; choose Off to disable thinking.", + "adaptiveOnlyDesc": "This model supports only adaptive thinking and cannot be disabled.", + "reasoningModeDesc": "Adaptive lets the model decide how much to think; budget uses a fixed token budget; off disables thinking.", + "providerTypeNotAvailable": "Provider type \"{{type}}\" is not available", + "selectOrEnterModel": "Select or enter model name", + "searchModel": "Search model...", + "searchLabel": "Search {{label}}...", + "openLabelList": "Open {{label}} list", + "noModelFound": "No model found.", + "useCustomModel": "Use \"{{search}}\" as custom model", + "useCustomValue": "Use \"{{search}}\" as custom {{label}}", + "noValueFound": "No {{label}} found.", + "selectReasoningMode": "Select reasoning mode", + "selectEffortLevel": "Select effort level (optional)", + "deletingProvider": "Deleting...", + "saveFailed": "An error occurred while saving", + "deleteFailed2": "An error occurred while deleting", + "testFailed": "An error occurred while testing", + "validationErrors": "Please fix the following validation errors:\n\n{{errors}}", + "unsavedChanges": "Unsaved changes", + "saveAndLeave": "Save and leave", + "discard": "Discard", + "providerActions": "Provider actions", + "reasoningHigh": "High", + "reasoningLow": "Low", + "reasoningMax": "Max", + "reasoningMedium": "Medium", + "reasoningExtraHigh": "Extra High", + "free": "free" + }, + "prompts": { + "title": "Prompts", + "agentPrompts": "Agent Prompts", + "toolPrompts": "Tool Prompts", + "agentPromptsDesc": "System and human prompts for AI agents", + "toolPromptsDesc": "Prompt templates for system tools and utilities", + "systemPrompt": "System Prompt", + "humanPrompt": "Human Prompt", + "promptTemplates": "Prompt Templates", + "template": "Template", + "agentName": "Agent Name", + "toolName": "Tool Name", + "prompt": "Prompt", + "custom": "Custom", + "default": "Default", + "noPrompts": "No prompts available", + "noPromptsDesc": "Prompt templates could not be loaded", + "loadingPrompts": "Loading prompts...", + "loadingPromptsDesc": "Please wait while we fetch your prompt templates", + "errorLoadingPrompts": "Error loading prompts", + "managePrompts": "Manage system and custom prompt templates", + "filterAgents": "Filter agents...", + "filterTools": "Filter tools...", + "resetSystem": "Reset System", + "resetHuman": "Reset Human", + "resetAll": "Reset All", + "resetting": "Resetting...", + "resetFailed": "Failed to reset prompt", + "resetPromptTitle": "Reset {{name}}", + "resetSystemDesc": "Are you sure you want to reset the system prompt for \"{{name}}\"? This will revert it to the default template and cannot be undone.", + "resetHumanDesc": "Are you sure you want to reset the human prompt for \"{{name}}\"? This will revert it to the default template and cannot be undone.", + "resetAllDesc": "Are you sure you want to reset all prompts for \"{{name}}\"? This will revert both system and human prompts to their default templates and cannot be undone.", + "resetGenericDesc": "Are you sure you want to reset the prompt for \"{{name}}\"? This will revert it to the default template and cannot be undone.", + "editPrompt": "Edit Prompt", + "savePrompt": "Save", + "validatePrompt": "Validate", + "validating": "Validating...", + "validationResults": "Validation Results", + "validationResultDesc": "The validation result for the {{tab}} prompt template.", + "validTemplate": "Valid Template", + "validationError": "Validation Error", + "diffTitle": "Diff", + "diffDesc": "Changes between current value and default template.", + "availableVariables": "Available variables", + "variablesHint": "Click to insert at the cursor, or cycle through existing uses.", + "systemPlaceholder": "Enter the system prompt template...", + "toolPlaceholder": "Enter the tool template...", + "humanPlaceholder": "Enter the human prompt template...", + "customizeAgentTemplates": "Customize the templates this agent uses", + "customizeToolTemplate": "Customize the template this tool uses", + "configureAgentPrompts": "Configure prompts for this AI agent", + "configureToolPrompt": "Configure the prompt for this tool", + "editPromptTitle": "Edit prompt", + "systemPromptLabel": "System Prompt", + "humanPromptLabel": "Human Prompt", + "loadingPromptData": "Loading prompt data...", + "loadingPromptDataDesc": "Please wait while we fetch prompt information", + "errorLoadingPromptData": "Error loading prompt data", + "promptNotFound": "Prompt not found", + "promptNotFoundDesc": "The prompt \"{{id}}\" could not be found or is not supported for editing.", + "resetPromptConfirm": "Reset Prompt", + "resetPromptDesc": "Are you sure you want to reset this prompt to its default value? This action cannot be undone.", + "saveFailed": "An error occurred while saving", + "resetFailed2": "An error occurred while resetting", + "validateFailed": "An error occurred while validating", + "humanTemplateRequired": "Human template is required", + "systemTemplateRequired": "System template is required", + "humanPromptTypeNotFound": "Human prompt type not found", + "promptActions": "Prompt actions", + "details": "Details", + "lineLabel": "Line", + "rawTooltip": "Edit the raw prompt template", + "variableGoToNext": "Go to next {{variable}} in the template", + "variableGoToNextCount": "Go to next {{variable}} in the template ({{count}} uses)", + "variableInsertAtCursor": "Insert {{variable}} at the cursor" + }, + "dashboard": { + "title": "Dashboard", + "analytics": "Analytics", + "overview": "Overview", + "week": "Week", + "month": "Month", + "quarter": "Quarter", + "totalFlows": "Total Flows", + "toolCalls": "Tool Calls", + "totalTokens": "Total Tokens", + "totalCost": "Total Cost", + "flowsSummary": "Tasks: {{tasks}} · Subtasks: {{subtasks}} · Assistants: {{assistants}}", + "totalDuration": "Total duration: {{duration}}", + "inputOutputTokens": "Input + Output tokens processed", + "totalLlmSpending": "Total LLM spending across all providers", + "usageByProvider": "Usage by Provider", + "usageByProviderDesc": "LLM token usage and costs grouped by provider", + "usageByModel": "Usage by Model", + "usageByModelDesc": "LLM token usage and costs grouped by model", + "usageByAgentType": "Usage by Agent Type", + "usageByAgentTypeDesc": "LLM token usage and costs grouped by agent type", + "toolCallsByFunction": "Tool Calls by Function", + "toolCallsByFunctionDesc": "Execution statistics for each tool function", + "function": "Function", + "type": "Type", + "count": "Count", + "totalDurationCol": "Total Duration", + "avgDuration": "Avg Duration", + "agentBadge": "Agent", + "toolBadge": "Tool", + "name": "Name", + "tokensIn": "Tokens In", + "tokensOut": "Tokens Out", + "cacheIn": "Cache In", + "cacheOut": "Cache Out", + "costIn": "Cost In", + "costOut": "Cost Out", + "totalCostCol": "Total Cost", + "noDataForPeriod": "No data for this period", + "flowsActivityOverTime": "Flows Activity Over Time", + "flowsActivityDesc": "Flows, tasks, and subtasks created per day", + "toolCallsOverTime": "Tool Calls Over Time", + "toolCallsOverTimeDesc": "Number of tool executions per day", + "tokenUsageOverTime": "Token Usage Over Time", + "tokenUsageOverTimeDesc": "Input and output tokens processed daily", + "costOverTime": "Cost Over Time", + "costOverTimeDesc": "LLM spending per day. May stay near zero when using local engines — this is expected.", + "flowsLegend": "Flows", + "tasksLegend": "Tasks", + "subtasksLegend": "Subtasks", + "toolCallsLegend": "Tool Calls", + "tokensInLegend": "Tokens In", + "tokensOutLegend": "Tokens Out", + "costInLegend": "Cost In", + "costOutLegend": "Cost Out", + "flowExecutionDetails": "Flow Execution Details", + "flowExecutionDetailsDesc": "Execution time and tool calls breakdown per flow", + "noFlowExecutions": "No flow executions in this period", + "flowNumber": "Flow #{{id}}", + "taskNumber": "Task #{{id}}", + "subtaskNumber": "Subtask #{{id}}", + "taskSingular": "task", + "taskPlural": "tasks", + "subtaskSingular": "subtask", + "subtaskPlural": "subtasks", + "assistantSingular": "assistant", + "assistantPlural": "assistants" + }, + "errors": { + "required": "This field is required", + "invalidEmail": "Invalid email address", + "minLength": "Minimum length is {{count}} characters", + "maxLength": "Maximum length is {{count}} characters", + "passwordsDontMatch": "Passwords don't match", + "somethingWentWrong": "Something went wrong", + "networkError": "Network error", + "unauthorized": "Unauthorized", + "forbidden": "Forbidden", + "notFound": "Not found", + "serverError": "Server error", + "chunkLoadError": "A new version was likely just deployed. Reloading will load the latest one.", + "domDesyncError": "The page hit a display glitch. Reloading usually clears it.", + "unexpectedError": "The page ran into an unexpected error. Reloading usually clears it.", + "couldntLoad": "Couldn't load" + }, + "success": { + "saved": "Saved successfully", + "deleted": "Deleted successfully", + "created": "Created successfully", + "updated": "Updated successfully", + "changesSaved": "Changes saved successfully" + }, + "templates": { + "title": "Templates", + "newTemplate": "New Template", + "loadingTemplates": "Loading templates...", + "loadingTemplatesDesc": "Please wait while we fetch your flow templates", + "errorLoadingTemplates": "Error loading templates", + "errorLoadingTemplate": "Error loading template", + "noTemplates": "No templates found", + "noTemplatesTitle": "No templates yet", + "noTemplatesDesc": "Create your first template to get started", + "filterTemplates": "Filter templates...", + "textColumn": "Text", + "openMenu": "Open menu", + "deleting": "Deleting...", + "renamedSuccess": "Template renamed successfully", + "titlePlaceholder": "Template title", + "templatesSheetTitle": "Templates", + "templateActions": "Template actions", + "doubleClickRename": "Double-click to rename", + "newTemplateBreadcrumb": "New template", + "templateFallback": "Template", + "view": "View", + "rawTooltip": "Edit the raw template", + "presetsTitle": "Preset templates", + "presetsHint": "Click a preset to fill the form, or expand it to preview the content.", + "createTitle": "Create a new template", + "editTitle": "Edit template", + "introDesc": "Add a title and content, or start from a preset.", + "titleInputPlaceholder": "A short name for this template", + "contentPlaceholder": "Describe the task, or start from a preset", + "contentAriaLabel": "Template content", + "titleRequired": "Title is required", + "textRequired": "Text is required", + "notFoundTitle": "Template not found", + "notFoundDesc": "The template you are looking for does not exist.", + "backToTemplates": "Back to Templates", + "replaceContentTitle": "Replace content?", + "replaceContentDesc": "Current form has content. Replace with the selected preset?", + "replace": "Replace", + "presetWebAppTitle": "Web Application Security Assessment", + "presetWebAppText": "Perform comprehensive security assessment of web application: {{TARGET_URL}}\n\nAction plan:\n1. Application Exploration: Navigate all pages, test features, identify endpoints and input vectors\n2. Vulnerability Testing per endpoint:\n - Path Traversal: attempt to read /etc/passwd, focus on file download/upload features\n - XSS: inject unique markers, scan responses, craft context-specific payloads\n - SQL Injection: run sqlmap on inputs, use tamper scripts for WAF bypass\n - Command Injection: use time-based detection, try commix utility\n - SSRF: use Interactsh for OOB, target file upload/PDF generation endpoints\n - XXE: test XML uploads and Office documents\n - Unsafe File Upload: test executable extensions, double extensions, null byte injection\n - CSRF: test token validation, POST to GET conversion\n3. Authentication & Session: test for broken authentication, session fixation, weak password policies\n4. Business Logic: identify privilege escalation, price manipulation, workflow bypass opportunities\n5. Report: document all findings with reproduction steps and proof-of-concept exploits", + "presetNetworkTitle": "Network Infrastructure Discovery & Mapping", + "presetNetworkText": "Perform network infrastructure reconnaissance of target: {{TARGET_NETWORK}}\n\nAction plan:\n1. Network Discovery: identify live hosts using nmap ping sweeps, map network topology\n2. Port Scanning: comprehensive port scan (1-65535), identify all open services\n3. Service Enumeration: fingerprint service versions, detect OS information\n4. Vulnerability Scanning: run automated vulnerability scans against discovered services\n5. SSL/TLS Analysis: check certificate validity, weak ciphers, protocol vulnerabilities\n6. Banner Grabbing: collect detailed service information for exploit research\n7. Network Diagram: create visual map of discovered infrastructure\n8. Report: prioritized list of hosts, services, and potential attack vectors", + "presetAdTitle": "Active Directory Penetration Test", + "presetAdText": "Conduct Active Directory security assessment for domain: {{DOMAIN_NAME}}\n\nAction plan:\n1. Initial Access: test password spraying, check for AS-REP roasting, look for Kerberoastable accounts\n2. Domain Enumeration: enumerate users, groups, computers, GPOs, trust relationships\n3. Privilege Escalation: identify misconfigured ACLs, check for exploitable group memberships, find delegation issues\n4. Credential Harvesting: search for credentials in SYSVOL, check for password in AD attributes, dump NTDS.dit if possible\n5. Lateral Movement: test pass-the-hash, pass-the-ticket, overpass-the-hash techniques\n6. Persistence: identify opportunities for golden ticket, silver ticket, DCSync rights\n7. Domain Admin Path: map attack path from current privileges to Domain Admin\n8. Report: document attack chain, compromised accounts, security gaps in AD configuration", + "presetApiTitle": "API Security Testing", + "presetApiText": "Perform comprehensive API security assessment: {{API_BASE_URL}}\n\nAction plan:\n1. API Discovery: identify all endpoints, HTTP methods, parameters\n2. Authentication Testing: test broken authentication, token manipulation, JWT vulnerabilities\n3. Authorization Testing: test broken object-level authorization (BOLA/IDOR), function-level authorization bypass\n4. Input Validation: test injection attacks (SQL, NoSQL, Command, XXE), mass assignment vulnerabilities\n5. Rate Limiting: test for absence of rate limiting, brute force protection\n6. Business Logic: test for excessive data exposure, lack of resource limiting, unsafe consumption of APIs\n7. Security Misconfiguration: check CORS policy, security headers, verbose error messages\n8. GraphQL Specific (if applicable): test introspection, query depth limits, batching attacks\n9. Report: document API vulnerabilities with curl/Postman proof-of-concepts", + "presetAwsTitle": "Cloud Infrastructure Security Audit (AWS)", + "presetAwsText": "Perform security audit of AWS infrastructure: {{AWS_ACCOUNT_ID or DOMAIN}}\n\nAction plan:\n1. Reconnaissance: identify S3 buckets, EC2 instances, public endpoints, enumerate services via DNS\n2. S3 Security: test bucket permissions, public access, ACL misconfigurations, bucket policies\n3. IAM Assessment: review roles, policies, check for overly permissive permissions, find unused credentials\n4. EC2 Security: scan for open security groups, test instance metadata service (169.254.169.254), check IMDSv2\n5. Network Security: review VPC configurations, security groups, NACLs, public subnets\n6. Database Exposure: check RDS public accessibility, security groups, encryption settings\n7. Lambda Functions: test for function URL exposure, environment variable leaks, IAM role permissions\n8. CloudTrail & Logging: verify logging is enabled, check for security monitoring gaps\n9. Report: prioritized cloud security findings with AWS-specific remediation steps", + "presetWordpressTitle": "WordPress Security Assessment", + "presetWordpressText": "Conduct WordPress security assessment: {{WORDPRESS_URL}}\n\nAction plan:\n1. Version Detection: identify WordPress core version, theme, and active plugins\n2. Plugin Vulnerabilities: enumerate installed plugins, check for known CVEs using WPScan and Sploitus\n3. Theme Vulnerabilities: identify theme version, search for known exploits\n4. User Enumeration: enumerate valid usernames via REST API, author archives, login responses\n5. Authentication Testing: test weak passwords, brute force protection, 2FA bypass\n6. File Upload: test media upload restrictions, arbitrary file upload vulnerabilities\n7. XML-RPC: check if enabled, test pingback SSRF, brute force amplification\n8. SQL Injection: test search functionality, custom query parameters, plugin-specific inputs\n9. XSS Testing: test comments, search, contact forms, custom fields\n10. Configuration Issues: check wp-config.php exposure, directory listing, sensitive file access\n11. Report: document WordPress-specific vulnerabilities with exploit steps", + "presetExternalTitle": "External Attack Surface Assessment", + "presetExternalText": "Perform external attack surface assessment for organization: {{ORGANIZATION_NAME or DOMAIN}}\n\nAction plan:\n1. Asset Discovery: enumerate all domains, subdomains (subfinder, amass), IP ranges, ASN information\n2. Certificate Transparency: search crt.sh for subdomains, identify forgotten assets\n3. Port Scanning: scan all discovered assets for open ports and services\n4. Web Application Fingerprinting: identify technologies, CMS, frameworks, server versions\n5. Email Security: test SPF, DKIM, DMARC records, email spoofing potential\n6. Cloud Asset Discovery: search for exposed S3 buckets, Azure blobs, exposed cloud databases\n7. Sensitive Data Exposure: search GitHub, GitLab, Pastebin for leaked credentials, API keys\n8. Third-Party Integrations: identify SaaS applications, API endpoints, partner integrations\n9. Vulnerability Prioritization: identify internet-facing critical vulnerabilities\n10. Report: comprehensive external attack surface map with risk-prioritized findings", + "presetInternalTitle": "Internal Network Penetration Test", + "presetInternalText": "Conduct internal network penetration test from position: {{INITIAL_ACCESS_LEVEL}}\n\nAction plan:\n1. Network Reconnaissance: ARP scanning, identify network segments, map internal infrastructure\n2. Service Discovery: comprehensive port scanning of internal hosts, identify critical servers\n3. SMB/NetBIOS Enumeration: test null sessions, enumerate shares, check for anonymous access\n4. Credential Attacks: LLMNR/NBT-NS poisoning (Responder), relay attacks, password spraying\n5. Vulnerability Exploitation: exploit unpatched services, test default credentials, known CVEs\n6. Privilege Escalation: exploit local vulnerabilities, misconfigured services, weak permissions\n7. Lateral Movement: pass-the-hash, token impersonation, exploit trust relationships\n8. Data Exfiltration: identify sensitive data locations, test data loss prevention controls\n9. Persistence: establish persistent access mechanisms\n10. Report: document internal security posture, attack path visualization, remediation priorities", + "presetMobileTitle": "Mobile Application Security Testing (API Backend)", + "presetMobileText": "Perform security testing of mobile application backend API: {{API_URL}}\n\nAction plan:\n1. Traffic Interception: analyze mobile app traffic, extract API endpoints and authentication\n2. Authentication Mechanisms: test OAuth flows, JWT implementation, refresh token handling, certificate pinning bypass\n3. API Endpoint Testing: test all discovered endpoints for BOLA/IDOR, broken function-level authorization\n4. Data Validation: test for injection attacks in API parameters, test file upload endpoints\n5. Business Logic: test premium feature bypass, subscription validation, in-app purchase verification\n6. Session Management: test token expiration, concurrent session handling, session fixation\n7. Sensitive Data: check for PII exposure, excessive data in responses, hardcoded secrets\n8. Rate Limiting: test brute force protection on login, API rate limits, account lockout\n9. Deep Linking: test for deep link hijacking, intent redirection (Android), URL scheme abuse (iOS)\n10. Report: mobile-specific vulnerabilities with mitigation recommendations", + "presetDevOpsTitle": "DevOps & CI/CD Pipeline Security", + "presetDevOpsText": "Assess DevOps infrastructure and CI/CD pipeline security: {{ORGANIZATION}}\n\nAction plan:\n1. Repository Security: scan GitHub/GitLab for exposed secrets, API keys, credentials in commit history\n2. CI/CD Configuration: review Jenkins/GitLab CI/GitHub Actions configurations, test for injection in pipeline definitions\n3. Container Security: scan Docker images for vulnerabilities, test for container escape, check image sources\n4. Secrets Management: test secret storage (HashiCorp Vault, AWS Secrets Manager), check for hardcoded secrets\n5. Access Control: review permissions on repositories, pipeline access, deployment keys, service accounts\n6. Artifact Security: scan build artifacts, test artifact repository access controls (Nexus, Artifactory)\n7. Kubernetes Security: review pod security policies, RBAC, network policies, exposed dashboards\n8. Infrastructure as Code: review Terraform/Ansible for misconfigurations, overly permissive IAM roles\n9. Monitoring & Logging: verify security logging, test log tampering, check for security monitoring gaps\n10. Report: DevOps security findings with secure pipeline recommendations", + "presetDatabaseTitle": "Database Security Assessment", + "presetDatabaseText": "Conduct database security assessment: {{DATABASE_TYPE}} at {{HOST:PORT}}\n\nAction plan:\n1. Access Testing: test for default credentials, weak passwords, anonymous access\n2. Network Exposure: verify database should not be internet-accessible, check firewall rules\n3. Authentication: test authentication mechanisms, user enumeration, password policies\n4. Authorization: review user permissions, test for privilege escalation, check for excessive grants\n5. Injection Testing: SQL injection in application layer, test stored procedures for injection\n6. Configuration Review: check for dangerous configuration options (xp_cmdshell, LOAD DATA, file_priv)\n7. Encryption: verify data-at-rest encryption, SSL/TLS for connections, check for sensitive data in plaintext\n8. Backup Security: test backup file access, check backup encryption, verify backup restoration procedures\n9. Audit Logging: verify audit logs enabled, test log tampering, check retention policies\n10. Report: database-specific security findings with hardening recommendations" + }, + "knowledges": { + "title": "Knowledges", + "newKnowledge": "New Knowledge", + "loadingKnowledges": "Loading knowledges...", + "loadingKnowledgesDesc": "Please wait while we fetch your knowledge documents", + "errorLoadingKnowledges": "Error loading knowledges", + "errorLoadingKnowledgeDoc": "Error loading knowledge document", + "noKnowledges": "No knowledges found", + "noKnowledgesTitle": "No knowledge documents yet", + "noKnowledgesDesc": "Create your first knowledge document to enrich the vector store", + "filterKnowledges": "Filter knowledges...", + "notFound": "Knowledge document not found", + "renamedSuccess": "Knowledge renamed successfully", + "typeColumn": "Type", + "questionColumn": "Question", + "flagsColumn": "Flags", + "flowBadge": "flow #{{id}}", + "manualBadge": "manual", + "agentBadge": "agent", + "openMenu": "Open menu", + "deleting": "Deleting...", + "questionPlaceholder": "Knowledge question", + "searchAriaLabel": "Search knowledge documents", + "semanticSearchPlaceholder": "Semantic search..." + }, + "resources": { + "title": "Resources", + "loadingResources": "Loading resources...", + "errorLoadingResources": "Error loading resources", + "noResources": "No resources found", + "noResourcesTitle": "No resources yet", + "noResourcesDesc": "Upload documents so PentAGI agents can reference them during your flows. You can also drag & drop files anywhere in this panel.", + "uploadSizeHint": "Up to 300 MB per file · 2 GB per upload", + "filterResources": "Filter resources...", + "pathCopied": "Path copied to clipboard", + "copyPathFailed": "Failed to copy path", + "itemsCopied": "{{count}} {{items}} copied to clipboard", + "copyPathsFailed": "Failed to copy paths", + "newFolder": "New folder", + "uploadFiles": "Upload files", + "uploading": "Uploading...", + "renameOrMove": "Rename or move", + "copyTo": "Copy to…", + "searchAriaLabel": "Search resources", + "searchPlaceholder": "Search resources...", + "clearSearchAriaLabel": "Clear resource search", + "columnSettingsAriaLabel": "Column settings", + "sizeColumn": "Size", + "modifiedColumn": "Modified", + "foldersFirst": "Folders first", + "relativeDates": "Relative dates", + "noMatchesTitle": "No matches", + "noMatchesDesc": "No resources match {{query}}. Try a different query.", + "dropFilesToUpload": "Drop files to upload", + "deleteDirectoryTitle": "Delete directory", + "deleteResourceTitle": "Delete resource" + }, + "unsavedChanges": { + "title": "Unsaved Changes", + "description": "You have unsaved changes. What would you like to do?", + "saveAndLeave": "Save and leave", + "discardChanges": "Discard changes", + "keepEditing": "Keep editing" + }, + "fileManager": { + "treeLabel": "File tree", + "selectAll": "Select all", + "collapseAll": "Collapse all", + "expandAll": "Expand all", + "columnName": "Name", + "columnSize": "Size", + "columnModified": "Modified", + "columnNameAria": "name", + "columnSizeAria": "size", + "columnModifiedAria": "modified date", + "sortByAscending": "Sort by {{label}} (ascending)", + "sortByDescending": "Sort by {{label}} (descending)", + "clearSorting": "Clear sorting on {{label}}", + "selectRow": "Select {{name}}", + "rowActions": "Row actions", + "download": "Download", + "copyPath": "Copy path", + "delete": "Delete", + "deleteConfirmTitle": "Delete {{count}}", + "deleteConfirmDescription": "This will delete {{count}}. This action cannot be undone.", + "copyPaths": "Copy paths", + "moveTo": "Move to…", + "copyTo": "Copy to…", + "saveAsResources": "Save as resources", + "cancel": "Cancel", + "moreActions": "More actions", + "selectedCount": "{{count}} selected", + "item": "item", + "items": "items" + }, + "terminal": { + "connecting": "Connecting...", + "disconnected": "Disconnected", + "reconnect": "Reconnect", + "clear": "Clear terminal", + "copy": "Copy", + "paste": "Paste", + "searchPlaceholder": "Search...", + "close": "Close terminal" + }, + "markdownEditor": { + "formatting": "Formatting", + "bold": "Bold", + "italic": "Italic", + "strikethrough": "Strikethrough", + "inlineCode": "Inline code", + "code": "Code", + "link": "Link", + "image": "Image", + "heading": "Heading", + "heading1": "Heading 1", + "heading2": "Heading 2", + "heading3": "Heading 3", + "heading4": "Heading 4", + "heading5": "Heading 5", + "heading6": "Heading 6", + "text": "Text", + "textStyle": "Text style", + "textStyleAria": "Text style: {{label}}", + "bulletList": "Bullet list", + "orderedList": "Ordered list", + "taskList": "Task list", + "checklist": "Checklist", + "lists": "Lists", + "listAria": "List: {{label}}", + "none": "None", + "table": "Table", + "quote": "Quote", + "blockquote": "Blockquote", + "codeBlock": "Code block", + "horizontalRule": "Horizontal rule", + "clearFormatting": "Clear formatting", + "undo": "Undo", + "redo": "Redo", + "editMode": "Edit", + "previewMode": "Preview", + "splitMode": "Split view", + "insertLink": "Insert link", + "editLink": "Edit link", + "removeLink": "Remove link", + "applyLink": "Apply link", + "openLinkInNewTab": "Open link in new tab", + "linkText": "Text", + "linkUrl": "Link URL", + "linkUrlPlaceholder": "https://example.com", + "linkUrlInvalid": "Only http, https, mailto and tel links are allowed.", + "insertImage": "Insert image", + "editImage": "Edit image", + "applyImage": "Apply image", + "removeImage": "Remove image", + "imageAlt": "Alt text (optional)", + "imageAltPlaceholder": "Describe the image", + "imageUrl": "Image URL", + "imageUrlPlaceholder": "https://example.com/image.png", + "imageUrlInvalid": "Only http(s) or base64 raster image URLs are allowed.", + "insertTable": "Insert table", + "insertRowAbove": "Insert row above", + "insertRowBelow": "Insert row below", + "insertColumnLeft": "Insert column left", + "insertColumnRight": "Insert column right", + "insertLeft": "Insert left", + "insertRight": "Insert right", + "insertAbove": "Insert above", + "insertBelow": "Insert below", + "addRowAbove": "Add row above", + "addRowBelow": "Add row below", + "addColumnLeft": "Add column left", + "addColumnRight": "Add column right", + "alignColumn": "Align column", + "clearContents": "Clear contents", + "headerRow": "Header row", + "columnActions": "Column actions", + "rowActions": "Row actions", + "deleteRow": "Delete row", + "deleteColumn": "Delete column", + "deleteTable": "Delete table", + "richEditor": "Rich editor", + "rawSource": "Raw source", + "writeSomething": "Write something…", + "apply": "Apply", + "remove": "Remove" + }, + "overwrite": { + "title": "Overwrite existing item?", + "description": "An item with this name already exists. Do you want to overwrite it?", + "overwrite": "Overwrite", + "keepBoth": "Keep both", + "skip": "Skip", + "replaceTitle": "Replace existing item?", + "replace": "Replace", + "replaceAll": "Replace all", + "singleConflictDescription": "An item named \"{{name}}\" already exists at /{{destination}}. Do you want to replace it?", + "batchConflictDescription": "{{count}} items already exist at the destination. Do you want to replace all of them?" + }, + "detailNavigation": { + "close": "Close", + "previous": "Previous", + "next": "Next", + "searchPlaceholder": "Search…", + "clearSearch": "Clear search", + "openList": "Open {{title}} list ({{position}})", + "showAllMatching": "Show all matching {{title}}", + "noItemsMatchQuery": "No items match \"{{query}}\".", + "noItemsMatchFilter": "No items match the current filter." + } +} diff --git a/frontend/src/locales/tr.json b/frontend/src/locales/tr.json new file mode 100644 index 000000000..d38d9be5f --- /dev/null +++ b/frontend/src/locales/tr.json @@ -0,0 +1,866 @@ +{ + "common": { + "dashboard": "Kontrol Paneli", + "flows": "Akışlar", + "templates": "Şablonlar", + "resources": "Kaynaklar", + "knowledges": "Bilgiler", + "settings": "Ayarlar", + "profile": "Profil", + "logout": "Çıkış Yap", + "newFlow": "Yeni Akış", + "recentFlows": "Son Akışlar", + "favoriteFlows": "Favori Akışlar", + "theme": "Tema", + "edit": "Düzenle", + "delete": "Sil", + "save": "Kaydet", + "cancel": "İptal", + "confirm": "Onayla", + "close": "Kapat", + "search": "Ara", + "filter": "Filtrele", + "loading": "Yükleniyor...", + "error": "Hata", + "success": "Başarılı", + "warning": "Uyarı", + "info": "Bilgi", + "yes": "Evet", + "no": "Hayır", + "back": "Geri", + "next": "İleri", + "previous": "Önceki", + "create": "Oluştur", + "update": "Güncelle", + "view": "Görüntüle", + "details": "Detaylar", + "actions": "İşlemler", + "status": "Durum", + "title": "Başlık", + "description": "Açıklama", + "name": "Ad", + "email": "E-posta", + "password": "Şifre", + "language": "Dil", + "english": "İngilizce", + "turkish": "Türkçe", + "selectLanguage": "Dil Seçin", + "rename": "Yeniden Adlandır", + "clone": "Klonla", + "reset": "Sıfırla", + "finish": "Bitir", + "test": "Test", + "validate": "Doğrula", + "diff": "Fark", + "reload": "Yenile", + "tryAgain": "Tekrar dene", + "notAvailable": "Yok", + "openMenu": "Menüyü aç", + "custom": "Özel", + "default": "Varsayılan", + "add": "Ekle", + "remove": "Kaldır", + "or": "veya", + "searchIn": "Şurada ara", + "columns": "Sütunlar", + "rowsPerPage": "Sayfa başına satır", + "firstPage": "İlk sayfa", + "previousPage": "Önceki sayfa", + "nextPage": "Sonraki sayfa", + "lastPage": "Son sayfa", + "clearSearch": "Aramayı temizle", + "allOption": "Tümü", + "filterEllipsis": "Filtrele...", + "pageOf": "Sayfa {{page}} / {{pageCount}}", + "noResults": "Sonuç yok.", + "noResultsPlain": "Sonuç yok", + "noMatches": "Eşleşme yok", + "noEntityYet": "Henüz {{entity}} yok", + "noEntityMatchPrefix": "Şununla eşleşen {{entity}} yok:", + "noEntityMatchSuffix": ". Farklı bir sorgu deneyin.", + "showingRange": "{{total}} kayıttan {{start}}–{{end}} arası gösteriliyor", + "noEntity": "{{entity}} yok", + "toggleSidebar": "Kenar Çubuğunu Aç/Kapat", + "dismissDialog": "İletişim kutusunu kapat", + "dismissSheet": "Paneli kapat", + "sidebar": "Kenar Çubuğu", + "more": "Daha fazla", + "displaysMobileSidebar": "Mobil kenar çubuğunu gösterir." + }, + "sidebar": { + "appName": "PentAGI", + "newFlow": "Yeni Akış", + "dashboard": "Kontrol Paneli", + "flows": "Akışlar", + "templates": "Şablonlar", + "resources": "Kaynaklar", + "knowledges": "Bilgiler", + "recentFlows": "Son Akışlar", + "favoriteFlows": "Favori Akışlar", + "settings": "Ayarlar", + "profile": "Profil", + "logout": "Çıkış Yap", + "theme": "Tema", + "newFlowAction": "Yeni akış", + "newTemplateAction": "Yeni şablon", + "newKnowledgeAction": "Yeni bilgi", + "uploadFile": "Dosya yükle", + "systemTheme": "Sistem teması", + "lightTheme": "Açık tema", + "darkTheme": "Koyu tema", + "toggleFavorite": "Favoriyi değiştir" + }, + "flow": { + "createNewFlow": "Yeni Akış Oluştur", + "flowTitle": "Akış Başlığı", + "flowDescription": "Akış Açıklaması", + "flowStatus": "Akış Durumu", + "running": "Çalışıyor", + "completed": "Tamamlandı", + "failed": "Başarısız", + "pending": "Bekliyor", + "created": "Oluşturuldu", + "waiting": "Bekliyor", + "finished": "Tamamlandı", + "startFlow": "Akışı Başlat", + "stopFlow": "Akışı Durdur", + "restartFlow": "Akışı Yeniden Başlat", + "deleteFlow": "Akışı Sil", + "editFlow": "Akışı Düzenle", + "viewDetails": "Detayları Görüntüle", + "newFlow": "Yeni akış", + "createFlow": "Yeni akış oluştur", + "describeFlow": "PentAGI'nin ne test etmesini istediğinizi açıklayın", + "automation": "Otomasyon", + "assistant": "Asistan", + "noFlowsFound": "Akış bulunamadı", + "getStarted": "İlk konuşma akışınızı oluşturarak başlayın", + "loadingFlows": "Akışlar yükleniyor...", + "loadingFlowsDesc": "Lütfen akışlarınız getirilirken bekleyin", + "errorLoadingFlows": "Akışlar yüklenirken hata oluştu", + "filterFlows": "Akışları filtrele...", + "id": "ID", + "provider": "Sağlayıcı", + "terminals": "Terminaller", + "noTerminals": "Terminal yok", + "renameSuccess": "Akış başarıyla yeniden adlandırıldı", + "renameFailed": "Akış yeniden adlandırılamadı", + "addToFavorites": "Favorilere ekle", + "removeFromFavorites": "Favorilerden kaldır", + "doubleClickRename": "Yeniden adlandırmak için çift tıklayın", + "selectFlow": "Bir akış seçin", + "report": "Rapor", + "openWebView": "Web görünümünde aç", + "copyToClipboard": "Panoya kopyala", + "downloadMD": "MD olarak indir", + "downloadPDF": "PDF olarak indir", + "reportCopied": "Rapor panoya kopyalandı", + "reportCopyFailed": "Rapor panoya kopyalanamadı", + "errorLoadingFlow": "Akış yüklenirken hata oluştu", + "creatingFlow": "Yeni akış oluşturuluyor...", + "describeAutomation": "PentAGI'nin ne test etmesini istediğinizi açıklayın...", + "describeAssistant": "Size nasıl yardımcı olmamı istersiniz?", + "finishing": "Tamamlanıyor...", + "deleting": "Siliniyor...", + "renaming": "Yeniden adlandırılıyor", + "toggleFavorite": "Favoriyi değiştir", + "openMenu": "Menüyü aç", + "flowActions": "Akış işlemleri", + "connected": "bağlı", + "disconnected": "bağlı değil", + "updatedAt": "Güncellendi", + "itemType": "akış", + "flowNumber": "Akış #{{id}}", + "loadingReport": "Rapor Yükleniyor...", + "generatingPdf": "PDF Oluşturuluyor...", + "preparingReportDesc": "Lütfen sızma testi raporunuz hazırlanırken bekleyin.", + "creatingPdfDesc": "PDF belgeniz oluşturuluyor. Bu birkaç dakika sürebilir.", + "errorLoadingReport": "Rapor Yüklenirken Hata Oluştu", + "failedToLoadFlowData": "Akış verisi yüklenemedi", + "unexpectedReportError": "Rapor yüklenirken beklenmeyen bir hata oluştu.", + "failedToGeneratePdf": "PDF oluşturulamadı", + "flowBreadcrumb": "Akış" + }, + "auth": { + "login": "Giriş Yap", + "signup": "Kayıt Ol", + "forgotPassword": "Şifremi Unuttum", + "rememberMe": "Beni Hatırla", + "username": "Kullanıcı Adı", + "confirmPassword": "Şifreyi Onayla", + "loginSuccess": "Giriş başarılı!", + "logoutSuccess": "Çıkış başarılı!", + "signIn": "Giriş Yap", + "updatePassword": "Şifreyi Güncelle", + "changePasswordRequired": "Devam etmeden önce şifrenizi değiştirmeniz gerekiyor.", + "loginLabel": "Giriş", + "enterEmail": "E-posta adresinizi girin", + "enterPassword": "Şifrenizi girin", + "invalidLogin": "Geçersiz giriş adı veya şifre", + "authFailed": "Kimlik doğrulama başarısız", + "continueWithGoogle": "Google ile devam et", + "continueWithGithub": "GitHub ile devam et", + "authInProgress": "Kimlik doğrulama devam ediyor...", + "authCompleteClosing": "Kimlik doğrulama tamamlandı, pencere kapatılıyor...", + "errorCommunicatingWithParent": "Üst pencereyle iletişimde hata oluştu. Birkaç saniye içinde kapatılıyor...", + "authWindowOpenedDirectly": "Kimlik doğrulama penceresi doğrudan açıldı. Giriş sayfasına yönlendiriliyor..." + }, + "settings": { + "general": "Genel", + "account": "Hesap", + "appearance": "Görünüm", + "languageSettings": "Dil Ayarları", + "notifications": "Bildirimler", + "privacy": "Gizlilik", + "apiTokens": "API Token'ları", + "providers": "Sağlayıcılar", + "prompts": "Prompt'lar", + "darkMode": "Karanlık Mod", + "lightMode": "Açık Mod", + "system": "Sistem", + "saveChanges": "Değişiklikleri Kaydet", + "backToApp": "Uygulamaya Dön", + "settingsTitle": "Ayarlar" + }, + "account": { + "title": "Hesap", + "displayName": "Görünen ad", + "displayNameDesc": "Uygulama genelinde gösterilen ad.", + "emailAddress": "E-posta adresi", + "emailLinkedFrom": "{{provider}} hesabınızdan bağlı.", + "emailSignIn": "Giriş yapmak için kullandığınız e-posta.", + "password": "Şifre", + "changePassword": "Hesap şifrenizi değiştirin.", + "change": "Değiştir", + "memberSince": "{{date}} tarihinden beri üye", + "localAccount": "Yerel hesap", + "oauthAccount": "OAuth hesabı", + "nameRequired": "Ad gereklidir", + "nameMaxLength": "Ad 70 karakteri geçemez", + "nameUpdated": "Ad başarıyla güncellendi", + "nameUpdateFailed": "Ad güncellenemedi", + "enterDisplayName": "Görünen adınızı girin", + "updateName": "Adı Güncelle", + "currentPassword": "Mevcut Şifre", + "newPassword": "Yeni Şifre", + "confirmNewPassword": "Yeni Şifreyi Onayla", + "enterCurrentPassword": "Mevcut şifrenizi girin", + "enterNewPassword": "Yeni şifrenizi girin", + "confirmNewPasswordPlaceholder": "Yeni şifrenizi onaylayın", + "updatePasswordButton": "Şifreyi Güncelle", + "passwordUpdated": "Şifre başarıyla değiştirildi", + "passwordUpdateFailed": "Şifre değiştirilemedi", + "passwordMinLength": "Şifre en az 8 karakter olmalıdır", + "passwordMaxLength": "Şifre 72 karakteri geçemez", + "passwordComplexity": "Şifre ya 15 karakterden uzun olmalı ya da en az 8 karakter, bir rakam, küçük harf, büyük harf ve özel karakter (!@#$&*) içermelidir", + "passwordsDontMatch": "Şifreler eşleşmiyor", + "passwordSameAsCurrent": "Yeni şifre mevcut şifreden farklı olmalıdır", + "passwordHint": "16+ karakter veya 8+ karakter, rakam, küçük harf, büyük harf ve özel karakter (!@#$&*) içermelidir", + "skipForNow": "Şimdilik atla", + "newEmail": "Yeni E-posta", + "enterNewEmail": "Yeni e-posta adresinizi girin", + "emailRequired": "E-posta gereklidir", + "emailInvalid": "Geçersiz e-posta adresi", + "emailMaxLength": "E-posta 50 karakteri geçemez", + "emailUpdated": "E-posta başarıyla güncellendi", + "emailUpdateFailed": "E-posta güncellenemedi", + "currentPasswordRequired": "Mevcut şifre gereklidir", + "updateEmail": "E-postayı Güncelle" + }, + "apiTokens": { + "title": "API Token'ları", + "createToken": "Token Oluştur", + "tokenName": "Ad", + "tokenId": "Token ID", + "status": "Durum", + "expires": "Son Kullanma", + "createdAt": "Oluşturuldu", + "active": "aktif", + "revoked": "iptal edildi", + "expired": "süresi doldu", + "unnamed": "(adsız)", + "tokenNamePlaceholder": "Token adı (isteğe bağlı)", + "pickDate": "Tarih seçin", + "copyTokenId": "Token ID'yi kopyala", + "openMenu": "Menüyü aç", + "filterTokens": "Token'ları filtrele...", + "noTokensTitle": "Yapılandırılmış API token'ı yok", + "noTokensDesc": "PentAGI'ye programatik erişim için ilk API token'ınızı oluşturun", + "loadingTokens": "Token'lar yükleniyor...", + "loadingTokensDesc": "Lütfen API token'larınız getirilirken bekleyin", + "errorLoadingTokens": "Token'lar yüklenirken hata oluştu", + "createTokenTitle": "API Token Oluşturuldu", + "createTokenDesc": "Bu token'ı şimdi kopyalayın. Güvenlik nedeniyle bir daha göremeyeceksiniz.", + "copyToken": "Token'ı Kopyala", + "tokenCopied": "Token panoya kopyalandı", + "tokenCopyFailed": "Token panoya kopyalanamadı", + "tokenIdCopied": "Token ID panoya kopyalandı", + "tokenIdCopyFailed": "Token ID panoya kopyalanamadı", + "createTokenFailed": "Token oluşturulamadı", + "updateTokenFailed": "Token güncellenemedi", + "deleteTokenFailed": "Token silinemedi", + "graphqlPlayground": "GraphQL Playground", + "swaggerUi": "Swagger UI", + "developerTools": "Geliştirici araçları", + "tokenNameMax": "Token adı 100 karakteri geçemez", + "expirationRequired": "Son kullanma tarihi gereklidir", + "submitting": "Gönderiliyor…", + "submit": "Gönder", + "deleteToken": "Token sil", + "deleting": "Siliniyor...", + "tokenNoun": "token" + }, + "providers": { + "title": "Sağlayıcılar", + "createProvider": "Sağlayıcı Oluştur", + "editProvider": "Sağlayıcıyı Düzenle", + "noProvidersTitle": "Yapılandırılmış sağlayıcı yok", + "noProvidersDesc": "İlk dil modeli sağlayıcınızı ekleyerek başlayın", + "addProvider": "Sağlayıcı Ekle", + "loadingProviders": "Sağlayıcılar yükleniyor...", + "loadingProvidersDesc": "Lütfen sağlayıcı yapılandırmalarınız getirilirken bekleyin", + "errorLoadingProviders": "Sağlayıcılar yüklenirken hata oluştu", + "filterProviders": "Sağlayıcıları filtrele...", + "noAvailableTypes": "Kullanılabilir sağlayıcı türü yok", + "name": "Ad", + "type": "Tür", + "createdAt": "Oluşturuldu", + "updatedAt": "Güncellendi", + "agentConfigurations": "Ajan Yapılandırmaları", + "noAgentConfig": "Ajan yapılandırması mevcut değil", + "noConfig": "Yapılandırma mevcut değil", + "deleteFailed": "Sağlayıcı silinemedi", + "deleteProviderTitle": "Sağlayıcıyı sil", + "cloneProvider": "Klonla", + "providerTypeRequired": "Sağlayıcı türü gereklidir", + "providerNameRequired": "Sağlayıcı adı gereklidir", + "providerNameMax": "Maksimum 50 karakter kullanılabilir", + "modelRequired": "Model gereklidir", + "minLengthExceed": "Minimum uzunluk maksimum uzunluğu geçemez", + "maxReasoningTokens": "Maksimum 32000 token", + "validJsonObject": "Geçerli bir JSON nesnesi olmalıdır", + "createNewProvider": "Yeni sağlayıcı oluştur", + "editProviderTitle": "Sağlayıcıyı düzenle", + "configureNewProvider": "Yeni bir dil modeli sağlayıcısı yapılandırın", + "updateProviderSettings": "Sağlayıcı ayarlarını ve yapılandırmasını güncelleyin", + "providerType": "Tür", + "providerTypeDesc": "Dil modeli sağlayıcısının türü", + "providerName": "Ad", + "providerNameDesc": "Sağlayıcı yapılandırmanız için benzersiz bir ad", + "selectProvider": "Sağlayıcı seçin", + "enterProviderName": "Sağlayıcı adı girin", + "loadingProviderData": "Sağlayıcı verisi yükleniyor...", + "loadingProviderDataDesc": "Lütfen sağlayıcı yapılandırması getirilirken bekleyin", + "errorLoadingProviderData": "Sağlayıcı verisi yüklenirken hata oluştu", + "testProvider": "Test", + "testing": "Test ediliyor...", + "testResults": "Sağlayıcı Test Sonuçları", + "passed": "geçti", + "model": "Model", + "temperature": "Sıcaklık", + "maxTokens": "Maksimum Token", + "topP": "Top P", + "topK": "Top K", + "minLength": "Minimum Uzunluk", + "maxLength": "Maksimum Uzunluk", + "repetitionPenalty": "Tekrar Cezası", + "frequencyPenalty": "Sıklık Cezası", + "presencePenalty": "Varlık Cezası", + "reasoningConfig": "Muhakeme Yapılandırması", + "reasoningMode": "Muhakeme Modu", + "reasoningEffort": "Muhakeme Çabası", + "reasoningMaxTokens": "Muhakeme Maksimum Token", + "priceConfig": "Fiyat Yapılandırması", + "inputPrice": "Giriş Fiyatı", + "outputPrice": "Çıkış Fiyatı", + "cacheReadPrice": "Önbellek Okuma Fiyatı", + "cacheWritePrice": "Önbellek Yazma Fiyatı", + "inputPriceDesc": "1M giriş token başına fiyat", + "outputPriceDesc": "1M çıkış token başına fiyat", + "cacheReadPriceDesc": "1M önbelleğe alınmış okuma token başına fiyat", + "cacheWritePriceDesc": "1M önbellek yazma token başına fiyat", + "extraBody": "Ek Gövde", + "extraBodyTitle": "Ek Gövde", + "extraBodyDesc": "Her çağrıya birleştirilen JSON nesnesi olarak sağlayıcıya özgü istek gövde alanları (ör. vLLM chat_template_kwargs).", + "extraBodyLabel": "Ek Gövde (JSON)", + "testResultSuccess": "Başarılı", + "testResultFailed": "Başarısız", + "testResultUnknown": "Bilinmiyor", + "reasoning": "Muhakeme", + "streaming": "Akış", + "noTestsAvailable": "Bu ajan için test mevcut değil", + "notSelected": "Seçilmedi", + "adaptive": "Adaptif", + "budget": "Bütçe", + "off": "Kapalı (düşünme yok)", + "adaptiveThinkingDesc": "Bu model adaptif olarak düşünür; düşünmeyi devre dışı bırakmak için Kapalı'yı seçin.", + "adaptiveOnlyDesc": "Bu model yalnızca adaptif düşünmeyi destekler ve devre dışı bırakılamaz.", + "reasoningModeDesc": "Adaptif, modelin ne kadar düşüneceğine karar vermesini sağlar; bütçe sabit bir token bütçesi kullanır; kapalı düşünmeyi devre dışı bırakır.", + "providerTypeNotAvailable": "\"{{type}}\" sağlayıcı türü kullanılamıyor", + "selectOrEnterModel": "Model seçin veya girin", + "searchModel": "Model ara...", + "searchLabel": "{{label}} ara...", + "openLabelList": "{{label}} listesini aç", + "noModelFound": "Model bulunamadı.", + "useCustomModel": "\"{{search}}\" değerini özel model olarak kullan", + "useCustomValue": "\"{{search}}\" değerini özel {{label}} olarak kullan", + "noValueFound": "{{label}} bulunamadı.", + "selectReasoningMode": "Muhakeme modu seçin", + "selectEffortLevel": "Çaba seviyesi seçin (isteğe bağlı)", + "deletingProvider": "Siliniyor...", + "saveFailed": "Kaydedilirken bir hata oluştu", + "deleteFailed2": "Silinirken bir hata oluştu", + "testFailed": "Test sırasında bir hata oluştu", + "validationErrors": "Lütfen aşağıdaki doğrulama hatalarını düzeltin:\n\n{{errors}}", + "unsavedChanges": "Kaydedilmemiş değişiklikler", + "saveAndLeave": "Kaydet ve çık", + "discard": "Değişiklikleri at", + "providerActions": "Sağlayıcı işlemleri", + "reasoningHigh": "Yüksek", + "reasoningLow": "Düşük", + "reasoningMax": "Maksimum", + "reasoningMedium": "Orta", + "reasoningExtraHigh": "Çok Yüksek", + "free": "ücretsiz" + }, + "prompts": { + "title": "Prompt'lar", + "agentPrompts": "Ajan Prompt'ları", + "toolPrompts": "Araç Prompt'ları", + "agentPromptsDesc": "Yapay zeka ajanları için sistem ve kullanıcı prompt'ları", + "toolPromptsDesc": "Sistem araçları ve yardımcı programlar için prompt şablonları", + "systemPrompt": "Sistem Prompt'u", + "humanPrompt": "Kullanıcı Prompt'u", + "promptTemplates": "Prompt Şablonları", + "template": "Şablon", + "agentName": "Ajan Adı", + "toolName": "Araç Adı", + "prompt": "Prompt", + "custom": "Özel", + "default": "Varsayılan", + "noPrompts": "Prompt bulunamadı", + "noPromptsDesc": "Prompt şablonları yüklenemedi", + "loadingPrompts": "Prompt'lar yükleniyor...", + "loadingPromptsDesc": "Lütfen prompt şablonlarınız getirilirken bekleyin", + "errorLoadingPrompts": "Prompt'lar yüklenirken hata oluştu", + "managePrompts": "Sistem ve özel prompt şablonlarını yönetin", + "filterAgents": "Ajanları filtrele...", + "filterTools": "Araçları filtrele...", + "resetSystem": "Sistemi Sıfırla", + "resetHuman": "Kullanıcıyı Sıfırla", + "resetAll": "Tümünü Sıfırla", + "resetting": "Sıfırlanıyor...", + "resetFailed": "Prompt sıfırlanamadı", + "resetPromptTitle": "{{name}} Sıfırla", + "resetSystemDesc": "\"{{name}}\" için sistem prompt'unu sıfırlamak istediğinizden emin misiniz? Bu işlem varsayılan şablona döndürecek ve geri alınamaz.", + "resetHumanDesc": "\"{{name}}\" için kullanıcı prompt'unu sıfırlamak istediğinizden emin misiniz? Bu işlem varsayılan şablona döndürecek ve geri alınamaz.", + "resetAllDesc": "\"{{name}}\" için tüm prompt'ları sıfırlamak istediğinizden emin misiniz? Bu işlem hem sistem hem de kullanıcı prompt'larını varsayılan şablonlarına döndürecek ve geri alınamaz.", + "resetGenericDesc": "\"{{name}}\" için prompt'u sıfırlamak istediğinizden emin misiniz? Bu işlem varsayılan şablona döndürecek ve geri alınamaz.", + "editPrompt": "Prompt Düzenle", + "savePrompt": "Kaydet", + "validatePrompt": "Doğrula", + "validating": "Doğrulanıyor...", + "validationResults": "Doğrulama Sonuçları", + "validationResultDesc": "{{tab}} prompt şablonu için doğrulama sonucu.", + "validTemplate": "Geçerli Şablon", + "validationError": "Doğrulama Hatası", + "diffTitle": "Fark", + "diffDesc": "Mevcut değer ile varsayılan şablon arasındaki değişiklikler.", + "availableVariables": "Kullanılabilir değişkenler", + "variablesHint": "İmlece eklemek için tıklayın veya mevcut kullanımlar arasında geçiş yapın.", + "systemPlaceholder": "Sistem prompt şablonunu girin...", + "toolPlaceholder": "Araç şablonunu girin...", + "humanPlaceholder": "Kullanıcı prompt şablonunu girin...", + "customizeAgentTemplates": "Bu ajanın kullandığı şablonları özelleştirin", + "customizeToolTemplate": "Bu aracın kullandığı şablonu özelleştirin", + "configureAgentPrompts": "Bu yapay zeka ajanı için prompt'ları yapılandırın", + "configureToolPrompt": "Bu araç için prompt'u yapılandırın", + "editPromptTitle": "Prompt düzenle", + "systemPromptLabel": "Sistem Prompt'u", + "humanPromptLabel": "Kullanıcı Prompt'u", + "loadingPromptData": "Prompt verisi yükleniyor...", + "loadingPromptDataDesc": "Lütfen prompt bilgisi getirilirken bekleyin", + "errorLoadingPromptData": "Prompt verisi yüklenirken hata oluştu", + "promptNotFound": "Prompt bulunamadı", + "promptNotFoundDesc": "\"{{id}}\" prompt'u bulunamadı veya düzenleme için desteklenmiyor.", + "resetPromptConfirm": "Prompt'u Sıfırla", + "resetPromptDesc": "Bu prompt'u varsayılan değerine sıfırlamak istediğinizden emin misiniz? Bu işlem geri alınamaz.", + "saveFailed": "Kaydedilirken bir hata oluştu", + "resetFailed2": "Sıfırlanırken bir hata oluştu", + "validateFailed": "Doğrulanırken bir hata oluştu", + "humanTemplateRequired": "Kullanıcı şablonu gereklidir", + "systemTemplateRequired": "Sistem şablonu gereklidir", + "humanPromptTypeNotFound": "Kullanıcı prompt türü bulunamadı", + "promptActions": "Prompt işlemleri", + "details": "Detaylar", + "lineLabel": "Satır", + "rawTooltip": "Ham prompt şablonunu düzenle", + "variableGoToNext": "Şablondaki bir sonraki {{variable}} konumuna git", + "variableGoToNextCount": "Şablondaki bir sonraki {{variable}} konumuna git ({{count}} kullanım)", + "variableInsertAtCursor": "{{variable}} öğesini imleç konumuna ekle" + }, + "dashboard": { + "title": "Kontrol Paneli", + "analytics": "Analitik", + "overview": "Genel Bakış", + "week": "Hafta", + "month": "Ay", + "quarter": "Çeyrek", + "totalFlows": "Toplam Akış", + "toolCalls": "Araç Çağrıları", + "totalTokens": "Toplam Token", + "totalCost": "Toplam Maliyet", + "flowsSummary": "Görevler: {{tasks}} · Alt Görevler: {{subtasks}} · Asistanlar: {{assistants}}", + "totalDuration": "Toplam süre: {{duration}}", + "inputOutputTokens": "İşlenen Giriş + Çıkış tokenleri", + "totalLlmSpending": "Tüm sağlayıcılardaki toplam LLM harcaması", + "usageByProvider": "Sağlayıcıya Göre Kullanım", + "usageByProviderDesc": "Sağlayıcıya göre gruplandırılmış LLM token kullanımı ve maliyetleri", + "usageByModel": "Modele Göre Kullanım", + "usageByModelDesc": "Modele göre gruplandırılmış LLM token kullanımı ve maliyetleri", + "usageByAgentType": "Ajan Türüne Göre Kullanım", + "usageByAgentTypeDesc": "Ajan türüne göre gruplandırılmış LLM token kullanımı ve maliyetleri", + "toolCallsByFunction": "Fonksiyona Göre Araç Çağrıları", + "toolCallsByFunctionDesc": "Her araç fonksiyonu için çalıştırma istatistikleri", + "function": "Fonksiyon", + "type": "Tür", + "count": "Sayı", + "totalDurationCol": "Toplam Süre", + "avgDuration": "Ortalama Süre", + "agentBadge": "Ajan", + "toolBadge": "Araç", + "name": "Ad", + "tokensIn": "Giriş Tokenleri", + "tokensOut": "Çıkış Tokenleri", + "cacheIn": "Önbellek Giriş", + "cacheOut": "Önbellek Çıkış", + "costIn": "Giriş Maliyeti", + "costOut": "Çıkış Maliyeti", + "totalCostCol": "Toplam Maliyet", + "noDataForPeriod": "Bu dönem için veri yok", + "flowsActivityOverTime": "Zaman İçinde Akış Etkinliği", + "flowsActivityDesc": "Günlük oluşturulan akışlar, görevler ve alt görevler", + "toolCallsOverTime": "Zaman İçinde Araç Çağrıları", + "toolCallsOverTimeDesc": "Günlük araç çalıştırma sayısı", + "tokenUsageOverTime": "Zaman İçinde Token Kullanımı", + "tokenUsageOverTimeDesc": "Günlük işlenen giriş ve çıkış tokenleri", + "costOverTime": "Zaman İçinde Maliyet", + "costOverTimeDesc": "Günlük LLM harcaması. Yerel motorlar kullanılırken sıfıra yakın kalabilir — bu beklenen bir durumdur.", + "flowsLegend": "Akışlar", + "tasksLegend": "Görevler", + "subtasksLegend": "Alt Görevler", + "toolCallsLegend": "Araç Çağrıları", + "tokensInLegend": "Giriş Tokenleri", + "tokensOutLegend": "Çıkış Tokenleri", + "costInLegend": "Giriş Maliyeti", + "costOutLegend": "Çıkış Maliyeti", + "flowExecutionDetails": "Akış Çalıştırma Detayları", + "flowExecutionDetailsDesc": "Akış başına çalıştırma süresi ve araç çağrısı dökümü", + "noFlowExecutions": "Bu dönemde akış çalıştırması yok", + "flowNumber": "Akış #{{id}}", + "taskNumber": "Görev #{{id}}", + "subtaskNumber": "Alt Görev #{{id}}", + "taskSingular": "görev", + "taskPlural": "görev", + "subtaskSingular": "alt görev", + "subtaskPlural": "alt görev", + "assistantSingular": "asistan", + "assistantPlural": "asistan" + }, + "errors": { + "required": "Bu alan gereklidir", + "invalidEmail": "Geçersiz e-posta adresi", + "minLength": "Minimum {{count}} karakter olmalıdır", + "maxLength": "Maksimum {{count}} karakter olmalıdır", + "passwordsDontMatch": "Şifreler eşleşmiyor", + "somethingWentWrong": "Bir şeyler yanlış gitti", + "networkError": "Ağ hatası", + "unauthorized": "Yetkisiz", + "forbidden": "Yasak", + "notFound": "Bulunamadı", + "serverError": "Sunucu hatası", + "chunkLoadError": "Muhtemelen yeni bir sürüm yayınlandı. Yenileme en son sürümü yükleyecektir.", + "domDesyncError": "Sayfada bir görüntüleme hatası oluştu. Yenileme genellikle düzeltiyor.", + "unexpectedError": "Sayfada beklenmedik bir hata oluştu. Yenileme genellikle düzeltiyor.", + "couldntLoad": "Yüklenemedi" + }, + "success": { + "saved": "Başarıyla kaydedildi", + "deleted": "Başarıyla silindi", + "created": "Başarıyla oluşturuldu", + "updated": "Başarıyla güncellendi", + "changesSaved": "Değişiklikler başarıyla kaydedildi" + }, + "templates": { + "title": "Şablonlar", + "newTemplate": "Yeni Şablon", + "loadingTemplates": "Şablonlar yükleniyor...", + "loadingTemplatesDesc": "Lütfen akış şablonlarınız getirilirken bekleyin", + "errorLoadingTemplates": "Şablonlar yüklenirken hata oluştu", + "errorLoadingTemplate": "Şablon yüklenirken hata oluştu", + "noTemplates": "Şablon bulunamadı", + "noTemplatesTitle": "Henüz şablon yok", + "noTemplatesDesc": "Başlamak için ilk şablonunuzu oluşturun", + "filterTemplates": "Şablonları filtrele...", + "textColumn": "Metin", + "openMenu": "Menüyü aç", + "deleting": "Siliniyor...", + "renamedSuccess": "Şablon başarıyla yeniden adlandırıldı", + "titlePlaceholder": "Şablon başlığı", + "templatesSheetTitle": "Şablonlar", + "templateActions": "Şablon işlemleri", + "doubleClickRename": "Yeniden adlandırmak için çift tıklayın", + "newTemplateBreadcrumb": "Yeni şablon", + "templateFallback": "Şablon", + "view": "Görünüm", + "rawTooltip": "Ham şablonu düzenle", + "presetsTitle": "Hazır şablonlar", + "presetsHint": "Formu doldurmak için bir hazır şablona tıklayın veya içeriği önizlemek için genişletin.", + "createTitle": "Yeni bir şablon oluştur", + "editTitle": "Şablonu düzenle", + "introDesc": "Bir başlık ve içerik ekleyin veya bir hazır şablondan başlayın.", + "titleInputPlaceholder": "Bu şablon için kısa bir ad", + "contentPlaceholder": "Görevi açıklayın veya bir hazır şablondan başlayın", + "contentAriaLabel": "Şablon içeriği", + "titleRequired": "Başlık gereklidir", + "textRequired": "Metin gereklidir", + "notFoundTitle": "Şablon bulunamadı", + "notFoundDesc": "Aradığınız şablon mevcut değil.", + "backToTemplates": "Şablonlara Dön", + "replaceContentTitle": "İçerik değiştirilsin mi?", + "replaceContentDesc": "Formda mevcut içerik var. Seçilen hazır şablonla değiştirilsin mi?", + "replace": "Değiştir", + "presetWebAppTitle": "Web Uygulaması Güvenlik Değerlendirmesi", + "presetWebAppText": "Web uygulamasının kapsamlı güvenlik değerlendirmesini yapın: {{TARGET_URL}}\n\nEylem planı:\n1. Uygulama Keşfi: Tüm sayfalarda gezinin, özellikleri test edin, uç noktaları ve girdi vektörlerini belirleyin\n2. Uç nokta başına Zafiyet Testi:\n - Path Traversal: /etc/passwd okumayı deneyin, dosya indirme/yükleme özelliklerine odaklanın\n - XSS: benzersiz işaretler enjekte edin, yanıtları tarayın, bağlama özgü yükler oluşturun\n - SQL Injection: girdilerde sqlmap çalıştırın, WAF atlatma için tamper script'leri kullanın\n - Command Injection: zaman tabanlı tespit kullanın, commix aracını deneyin\n - SSRF: OOB için Interactsh kullanın, dosya yükleme/PDF oluşturma uç noktalarını hedefleyin\n - XXE: XML yüklemelerini ve Office belgelerini test edin\n - Güvensiz Dosya Yükleme: çalıştırılabilir uzantıları, çift uzantıları, null byte enjeksiyonunu test edin\n - CSRF: token doğrulamasını, POST'tan GET'e dönüşümü test edin\n3. Kimlik Doğrulama & Oturum: bozuk kimlik doğrulama, oturum sabitleme, zayıf parola politikalarını test edin\n4. İş Mantığı: yetki yükseltme, fiyat manipülasyonu, iş akışı atlatma fırsatlarını belirleyin\n5. Rapor: tüm bulguları yeniden üretim adımları ve kanıt amaçlı istismarlarla belgeleyin", + "presetNetworkTitle": "Ağ Altyapısı Keşfi ve Haritalama", + "presetNetworkText": "Hedefin ağ altyapısı keşfini yapın: {{TARGET_NETWORK}}\n\nEylem planı:\n1. Ağ Keşfi: nmap ping taramalarıyla canlı hostları belirleyin, ağ topolojisini haritalayın\n2. Port Tarama: kapsamlı port taraması (1-65535), tüm açık servisleri belirleyin\n3. Servis Numaralandırma: servis sürümlerini tespit edin, işletim sistemi bilgisini bulun\n4. Zafiyet Taraması: bulunan servislere karşı otomatik zafiyet taramaları çalıştırın\n5. SSL/TLS Analizi: sertifika geçerliliğini, zayıf şifrelemeleri, protokol zafiyetlerini kontrol edin\n6. Banner Toplama: istismar araştırması için detaylı servis bilgisi toplayın\n7. Ağ Diyagramı: bulunan altyapının görsel haritasını oluşturun\n8. Rapor: öncelik sırasına konmuş host, servis ve olası saldırı vektörleri listesi", + "presetAdTitle": "Active Directory Sızma Testi", + "presetAdText": "Etki alanı için Active Directory güvenlik değerlendirmesi yapın: {{DOMAIN_NAME}}\n\nEylem planı:\n1. İlk Erişim: parola spreyleme test edin, AS-REP roasting'i kontrol edin, Kerberoast edilebilir hesapları arayın\n2. Etki Alanı Numaralandırma: kullanıcıları, grupları, bilgisayarları, GPO'ları, güven ilişkilerini numaralandırın\n3. Yetki Yükseltme: yanlış yapılandırılmış ACL'leri belirleyin, istismar edilebilir grup üyeliklerini kontrol edin, delegasyon sorunlarını bulun\n4. Kimlik Bilgisi Toplama: SYSVOL'de kimlik bilgilerini arayın, AD özniteliklerinde parolayı kontrol edin, mümkünse NTDS.dit'i dökün\n5. Yanal Hareket: pass-the-hash, pass-the-ticket, overpass-the-hash tekniklerini test edin\n6. Kalıcılık: golden ticket, silver ticket, DCSync hakları için fırsatları belirleyin\n7. Domain Admin Yolu: mevcut yetkilerden Domain Admin'e saldırı yolunu haritalayın\n8. Rapor: saldırı zincirini, ele geçirilen hesapları, AD yapılandırmasındaki güvenlik açıklarını belgeleyin", + "presetApiTitle": "API Güvenlik Testi", + "presetApiText": "Kapsamlı API güvenlik değerlendirmesi yapın: {{API_BASE_URL}}\n\nEylem planı:\n1. API Keşfi: tüm uç noktaları, HTTP metotlarını, parametreleri belirleyin\n2. Kimlik Doğrulama Testi: bozuk kimlik doğrulama, token manipülasyonu, JWT zafiyetlerini test edin\n3. Yetkilendirme Testi: nesne düzeyinde bozuk yetkilendirme (BOLA/IDOR), fonksiyon düzeyinde yetkilendirme atlatmayı test edin\n4. Girdi Doğrulama: enjeksiyon saldırılarını (SQL, NoSQL, Command, XXE), toplu atama zafiyetlerini test edin\n5. Hız Sınırlama: hız sınırlaması eksikliğini, brute force korumasını test edin\n6. İş Mantığı: aşırı veri ifşasını, kaynak sınırlama eksikliğini, güvensiz API tüketimini test edin\n7. Güvenlik Yanlış Yapılandırması: CORS politikasını, güvenlik başlıklarını, ayrıntılı hata mesajlarını kontrol edin\n8. GraphQL Özel (varsa): introspection'ı, sorgu derinliği sınırlarını, batching saldırılarını test edin\n9. Rapor: API zafiyetlerini curl/Postman kanıt amaçlı örnekleriyle belgeleyin", + "presetAwsTitle": "Bulut Altyapısı Güvenlik Denetimi (AWS)", + "presetAwsText": "AWS altyapısının güvenlik denetimini yapın: {{AWS_ACCOUNT_ID veya DOMAIN}}\n\nEylem planı:\n1. Keşif: S3 bucket'larını, EC2 örneklerini, açık uç noktaları belirleyin, DNS üzerinden servisleri numaralandırın\n2. S3 Güvenliği: bucket izinlerini, herkese açık erişimi, ACL yanlış yapılandırmalarını, bucket politikalarını test edin\n3. IAM Değerlendirmesi: rolleri, politikaları inceleyin, aşırı izin verilen izinleri kontrol edin, kullanılmayan kimlik bilgilerini bulun\n4. EC2 Güvenliği: açık güvenlik gruplarını tarayın, instance metadata servisini (169.254.169.254) test edin, IMDSv2'yi kontrol edin\n5. Ağ Güvenliği: VPC yapılandırmalarını, güvenlik gruplarını, NACL'leri, herkese açık subnet'leri inceleyin\n6. Veritabanı İfşası: RDS herkese açık erişilebilirliğini, güvenlik gruplarını, şifreleme ayarlarını kontrol edin\n7. Lambda Fonksiyonları: fonksiyon URL ifşasını, ortam değişkeni sızıntılarını, IAM rol izinlerini test edin\n8. CloudTrail & Loglama: loglamanın etkin olduğunu doğrulayın, güvenlik izleme boşluklarını kontrol edin\n9. Rapor: AWS'ye özgü iyileştirme adımlarıyla öncelikli bulut güvenliği bulguları", + "presetWordpressTitle": "WordPress Güvenlik Değerlendirmesi", + "presetWordpressText": "WordPress güvenlik değerlendirmesi yapın: {{WORDPRESS_URL}}\n\nEylem planı:\n1. Sürüm Tespiti: WordPress çekirdek sürümünü, temayı ve aktif eklentileri belirleyin\n2. Eklenti Zafiyetleri: yüklü eklentileri numaralandırın, WPScan ve Sploitus ile bilinen CVE'leri kontrol edin\n3. Tema Zafiyetleri: tema sürümünü belirleyin, bilinen istismarları arayın\n4. Kullanıcı Numaralandırma: REST API, yazar arşivleri, giriş yanıtları üzerinden geçerli kullanıcı adlarını numaralandırın\n5. Kimlik Doğrulama Testi: zayıf parolaları, brute force korumasını, 2FA atlatmayı test edin\n6. Dosya Yükleme: medya yükleme kısıtlamalarını, keyfi dosya yükleme zafiyetlerini test edin\n7. XML-RPC: etkin olup olmadığını kontrol edin, pingback SSRF'i, brute force amplifikasyonunu test edin\n8. SQL Injection: arama işlevini, özel sorgu parametrelerini, eklentiye özgü girdileri test edin\n9. XSS Testi: yorumları, aramayı, iletişim formlarını, özel alanları test edin\n10. Yapılandırma Sorunları: wp-config.php ifşasını, dizin listelemeyi, hassas dosya erişimini kontrol edin\n11. Rapor: WordPress'e özgü zafiyetleri istismar adımlarıyla belgeleyin", + "presetExternalTitle": "Dış Saldırı Yüzeyi Değerlendirmesi", + "presetExternalText": "Kuruluş için dış saldırı yüzeyi değerlendirmesi yapın: {{ORGANIZATION_NAME veya DOMAIN}}\n\nEylem planı:\n1. Varlık Keşfi: tüm alan adlarını, alt alan adlarını (subfinder, amass), IP aralıklarını, ASN bilgisini numaralandırın\n2. Sertifika Şeffaflığı: alt alan adları için crt.sh'yi arayın, unutulmuş varlıkları belirleyin\n3. Port Tarama: bulunan tüm varlıkları açık port ve servisler için tarayın\n4. Web Uygulaması Parmak İzi Çıkarma: teknolojileri, CMS'leri, framework'leri, sunucu sürümlerini belirleyin\n5. E-posta Güvenliği: SPF, DKIM, DMARC kayıtlarını, e-posta sahteciliği potansiyelini test edin\n6. Bulut Varlık Keşfi: ifşa olmuş S3 bucket'larını, Azure blob'larını, ifşa olmuş bulut veritabanlarını arayın\n7. Hassas Veri İfşası: sızdırılmış kimlik bilgileri, API anahtarları için GitHub, GitLab, Pastebin'i arayın\n8. Üçüncü Taraf Entegrasyonları: SaaS uygulamalarını, API uç noktalarını, iş ortağı entegrasyonlarını belirleyin\n9. Zafiyet Önceliklendirme: internete açık kritik zafiyetleri belirleyin\n10. Rapor: risk önceliklendirmeli kapsamlı dış saldırı yüzeyi haritası", + "presetInternalTitle": "İç Ağ Sızma Testi", + "presetInternalText": "Şu konumdan iç ağ sızma testi yapın: {{INITIAL_ACCESS_LEVEL}}\n\nEylem planı:\n1. Ağ Keşfi: ARP taraması, ağ segmentlerini belirleyin, iç altyapıyı haritalayın\n2. Servis Keşfi: iç hostların kapsamlı port taraması, kritik sunucuları belirleyin\n3. SMB/NetBIOS Numaralandırma: null oturumları test edin, paylaşımları numaralandırın, anonim erişimi kontrol edin\n4. Kimlik Bilgisi Saldırıları: LLMNR/NBT-NS zehirlemesi (Responder), relay saldırıları, parola spreyleme\n5. Zafiyet İstismarı: yamalanmamış servisleri istismar edin, varsayılan kimlik bilgilerini, bilinen CVE'leri test edin\n6. Yetki Yükseltme: yerel zafiyetleri, yanlış yapılandırılmış servisleri, zayıf izinleri istismar edin\n7. Yanal Hareket: pass-the-hash, token taklidi, güven ilişkilerini istismar edin\n8. Veri Sızdırma: hassas veri konumlarını belirleyin, veri kaybı önleme kontrollerini test edin\n9. Kalıcılık: kalıcı erişim mekanizmaları kurun\n10. Rapor: iç güvenlik duruşunu, saldırı yolu görselleştirmesini, iyileştirme önceliklerini belgeleyin", + "presetMobileTitle": "Mobil Uygulama Güvenlik Testi (API Backend)", + "presetMobileText": "Mobil uygulama backend API'sinin güvenlik testini yapın: {{API_URL}}\n\nEylem planı:\n1. Trafik Yakalama: mobil uygulama trafiğini analiz edin, API uç noktalarını ve kimlik doğrulamayı çıkarın\n2. Kimlik Doğrulama Mekanizmaları: OAuth akışlarını, JWT uygulamasını, yenileme token işlemeyi, sertifika pinleme atlatmayı test edin\n3. API Uç Nokta Testi: bulunan tüm uç noktaları BOLA/IDOR, bozuk fonksiyon düzeyi yetkilendirme için test edin\n4. Veri Doğrulama: API parametrelerinde enjeksiyon saldırılarını, dosya yükleme uç noktalarını test edin\n5. İş Mantığı: premium özellik atlatmayı, abonelik doğrulamasını, uygulama içi satın alma doğrulamasını test edin\n6. Oturum Yönetimi: token süresini, eşzamanlı oturum işlemeyi, oturum sabitlemeyi test edin\n7. Hassas Veri: PII ifşasını, yanıtlardaki aşırı veriyi, sabit kodlanmış sırları kontrol edin\n8. Hız Sınırlama: girişte brute force korumasını, API hız sınırlarını, hesap kilitlenmesini test edin\n9. Derin Bağlantı: derin bağlantı ele geçirmeyi, intent yönlendirmesini (Android), URL şeması istismarını (iOS) test edin\n10. Rapor: iyileştirme önerileriyle mobil'e özgü zafiyetler", + "presetDevOpsTitle": "DevOps ve CI/CD Pipeline Güvenliği", + "presetDevOpsText": "DevOps altyapısını ve CI/CD pipeline güvenliğini değerlendirin: {{ORGANIZATION}}\n\nEylem planı:\n1. Repository Güvenliği: commit geçmişinde ifşa olmuş sırları, API anahtarlarını, kimlik bilgilerini GitHub/GitLab'de tarayın\n2. CI/CD Yapılandırması: Jenkins/GitLab CI/GitHub Actions yapılandırmalarını inceleyin, pipeline tanımlarında enjeksiyonu test edin\n3. Konteyner Güvenliği: Docker imajlarını zafiyetler için tarayın, konteyner kaçışını test edin, imaj kaynaklarını kontrol edin\n4. Sır Yönetimi: sır depolamayı (HashiCorp Vault, AWS Secrets Manager) test edin, sabit kodlanmış sırları kontrol edin\n5. Erişim Kontrolü: repository izinlerini, pipeline erişimini, dağıtım anahtarlarını, servis hesaplarını inceleyin\n6. Artifact Güvenliği: build artifact'larını tarayın, artifact repository erişim kontrollerini (Nexus, Artifactory) test edin\n7. Kubernetes Güvenliği: pod güvenlik politikalarını, RBAC'ı, ağ politikalarını, ifşa olmuş dashboard'ları inceleyin\n8. Infrastructure as Code: Terraform/Ansible'ı yanlış yapılandırmalar, aşırı izin verilen IAM rolleri için inceleyin\n9. İzleme & Loglama: güvenlik loglamasını doğrulayın, log kurcalamayı test edin, güvenlik izleme boşluklarını kontrol edin\n10. Rapor: güvenli pipeline önerileriyle DevOps güvenlik bulguları", + "presetDatabaseTitle": "Veritabanı Güvenlik Değerlendirmesi", + "presetDatabaseText": "Veritabanı güvenlik değerlendirmesi yapın: {{DATABASE_TYPE}} - {{HOST:PORT}}\n\nEylem planı:\n1. Erişim Testi: varsayılan kimlik bilgilerini, zayıf parolaları, anonim erişimi test edin\n2. Ağ İfşası: veritabanının internete açık olmaması gerektiğini doğrulayın, güvenlik duvarı kurallarını kontrol edin\n3. Kimlik Doğrulama: kimlik doğrulama mekanizmalarını, kullanıcı numaralandırmayı, parola politikalarını test edin\n4. Yetkilendirme: kullanıcı izinlerini inceleyin, yetki yükseltmeyi test edin, aşırı hakları kontrol edin\n5. Enjeksiyon Testi: uygulama katmanında SQL injection, saklı yordamlarda enjeksiyonu test edin\n6. Yapılandırma İncelemesi: tehlikeli yapılandırma seçeneklerini (xp_cmdshell, LOAD DATA, file_priv) kontrol edin\n7. Şifreleme: bekleyen veri şifrelemesini, bağlantılar için SSL/TLS'i doğrulayın, düz metin hassas veriyi kontrol edin\n8. Yedekleme Güvenliği: yedek dosya erişimini test edin, yedek şifrelemesini kontrol edin, yedek geri yükleme prosedürlerini doğrulayın\n9. Denetim Loglaması: denetim loglarının etkin olduğunu doğrulayın, log kurcalamayı test edin, saklama politikalarını kontrol edin\n10. Rapor: sertleştirme önerileriyle veritabanına özgü güvenlik bulguları" + }, + "knowledges": { + "title": "Bilgiler", + "newKnowledge": "Yeni Bilgi", + "loadingKnowledges": "Bilgiler yükleniyor...", + "loadingKnowledgesDesc": "Lütfen bilgi belgeleriniz getirilirken bekleyin", + "errorLoadingKnowledges": "Bilgiler yüklenirken hata oluştu", + "errorLoadingKnowledgeDoc": "Bilgi belgesi yüklenirken hata oluştu", + "noKnowledges": "Bilgi bulunamadı", + "noKnowledgesTitle": "Henüz bilgi belgesi yok", + "noKnowledgesDesc": "Vektör deposunu zenginleştirmek için ilk bilgi belgenizi oluşturun", + "filterKnowledges": "Bilgileri filtrele...", + "notFound": "Bilgi belgesi bulunamadı", + "renamedSuccess": "Bilgi başarıyla yeniden adlandırıldı", + "typeColumn": "Tür", + "questionColumn": "Soru", + "flagsColumn": "Bayraklar", + "flowBadge": "akış #{{id}}", + "manualBadge": "manuel", + "agentBadge": "ajan", + "openMenu": "Menüyü aç", + "deleting": "Siliniyor...", + "questionPlaceholder": "Bilgi sorusu", + "searchAriaLabel": "Bilgi belgelerinde ara", + "semanticSearchPlaceholder": "Anlamsal arama..." + }, + "resources": { + "title": "Kaynaklar", + "loadingResources": "Kaynaklar yükleniyor...", + "errorLoadingResources": "Kaynaklar yüklenirken hata oluştu", + "noResources": "Kaynak bulunamadı", + "noResourcesTitle": "Henüz kaynak yok", + "noResourcesDesc": "PentAGI ajanlarının akışlarınız sırasında başvurabilmesi için belgeler yükleyin. Ayrıca dosyaları bu panelin herhangi bir yerine sürükleyip bırakabilirsiniz.", + "uploadSizeHint": "Dosya başına 300 MB'a, yükleme başına 2 GB'a kadar", + "filterResources": "Kaynakları filtrele...", + "pathCopied": "Yol panoya kopyalandı", + "copyPathFailed": "Yol kopyalanamadı", + "itemsCopied": "{{count}} {{items}} panoya kopyalandı", + "copyPathsFailed": "Yollar kopyalanamadı", + "newFolder": "Yeni klasör", + "uploadFiles": "Dosya yükle", + "uploading": "Yükleniyor...", + "renameOrMove": "Yeniden adlandır veya taşı", + "copyTo": "Şuraya kopyala…", + "searchAriaLabel": "Kaynaklarda ara", + "searchPlaceholder": "Kaynaklarda ara...", + "clearSearchAriaLabel": "Kaynak aramasını temizle", + "columnSettingsAriaLabel": "Sütun ayarları", + "sizeColumn": "Boyut", + "modifiedColumn": "Değiştirilme", + "foldersFirst": "Önce klasörler", + "relativeDates": "Göreli tarihler", + "noMatchesTitle": "Eşleşme yok", + "noMatchesDesc": "{{query}} ile eşleşen kaynak yok. Farklı bir sorgu deneyin.", + "dropFilesToUpload": "Yüklemek için dosyaları bırakın", + "deleteDirectoryTitle": "Dizini sil", + "deleteResourceTitle": "Kaynağı sil" + }, + "unsavedChanges": { + "title": "Kaydedilmemiş Değişiklikler", + "description": "Kaydedilmemiş değişiklikleriniz var. Ne yapmak istersiniz?", + "saveAndLeave": "Kaydet ve çık", + "discardChanges": "Değişiklikleri at", + "keepEditing": "Düzenlemeye devam et" + }, + "fileManager": { + "treeLabel": "Dosya ağacı", + "selectAll": "Tümünü seç", + "collapseAll": "Tümünü daralt", + "expandAll": "Tümünü genişlet", + "columnName": "Ad", + "columnSize": "Boyut", + "columnModified": "Değiştirilme", + "columnNameAria": "ad", + "columnSizeAria": "boyut", + "columnModifiedAria": "değiştirilme tarihi", + "sortByAscending": "{{label}} göre sırala (artan)", + "sortByDescending": "{{label}} göre sırala (azalan)", + "clearSorting": "{{label}} sıralamasını temizle", + "selectRow": "{{name}} öğesini seç", + "rowActions": "Satır eylemleri", + "download": "İndir", + "copyPath": "Yolu kopyala", + "delete": "Sil", + "deleteConfirmTitle": "{{count}} sil", + "deleteConfirmDescription": "Bu işlem {{count}} silecek. Bu işlem geri alınamaz.", + "copyPaths": "Yolları kopyala", + "moveTo": "Taşı…", + "copyTo": "Kopyala…", + "saveAsResources": "Kaynak olarak kaydet", + "cancel": "İptal", + "moreActions": "Diğer eylemler", + "selectedCount": "{{count}} seçildi", + "item": "öğe", + "items": "öğe" + }, + "terminal": { + "connecting": "Bağlanıyor...", + "disconnected": "Bağlantı kesildi", + "reconnect": "Yeniden bağlan", + "clear": "Terminali temizle", + "copy": "Kopyala", + "paste": "Yapıştır", + "searchPlaceholder": "Ara...", + "close": "Terminali kapat" + }, + "markdownEditor": { + "formatting": "Biçimlendirme", + "bold": "Kalın", + "italic": "İtalik", + "strikethrough": "Üstü çizili", + "inlineCode": "Satır içi kod", + "code": "Kod", + "link": "Bağlantı", + "image": "Görsel", + "heading": "Başlık", + "heading1": "Başlık 1", + "heading2": "Başlık 2", + "heading3": "Başlık 3", + "heading4": "Başlık 4", + "heading5": "Başlık 5", + "heading6": "Başlık 6", + "text": "Metin", + "textStyle": "Metin stili", + "textStyleAria": "Metin stili: {{label}}", + "bulletList": "Madde işaretli liste", + "orderedList": "Numaralı liste", + "taskList": "Görev listesi", + "checklist": "Kontrol listesi", + "lists": "Listeler", + "listAria": "Liste: {{label}}", + "none": "Yok", + "table": "Tablo", + "quote": "Alıntı", + "blockquote": "Alıntı", + "codeBlock": "Kod bloğu", + "horizontalRule": "Yatay çizgi", + "clearFormatting": "Biçimlendirmeyi temizle", + "undo": "Geri al", + "redo": "Yinele", + "editMode": "Düzenle", + "previewMode": "Önizleme", + "splitMode": "Bölünmüş görünüm", + "insertLink": "Bağlantı ekle", + "editLink": "Bağlantıyı düzenle", + "removeLink": "Bağlantıyı kaldır", + "applyLink": "Bağlantıyı uygula", + "openLinkInNewTab": "Bağlantıyı yeni sekmede aç", + "linkText": "Metin", + "linkUrl": "Bağlantı URL'si", + "linkUrlPlaceholder": "https://example.com", + "linkUrlInvalid": "Yalnızca http, https, mailto ve tel bağlantılarına izin verilir.", + "insertImage": "Görsel ekle", + "editImage": "Görseli düzenle", + "applyImage": "Görseli uygula", + "removeImage": "Görseli kaldır", + "imageAlt": "Alternatif metin (opsiyonel)", + "imageAltPlaceholder": "Görseli açıklayın", + "imageUrl": "Görsel URL'si", + "imageUrlPlaceholder": "https://example.com/image.png", + "imageUrlInvalid": "Yalnızca http(s) veya base64 raster görsel URL'lerine izin verilir.", + "insertTable": "Tablo ekle", + "insertRowAbove": "Üste satır ekle", + "insertRowBelow": "Alta satır ekle", + "insertColumnLeft": "Sola sütun ekle", + "insertColumnRight": "Sağa sütun ekle", + "insertLeft": "Sola ekle", + "insertRight": "Sağa ekle", + "insertAbove": "Üste ekle", + "insertBelow": "Alta ekle", + "addRowAbove": "Üste satır ekle", + "addRowBelow": "Alta satır ekle", + "addColumnLeft": "Sola sütun ekle", + "addColumnRight": "Sağa sütun ekle", + "alignColumn": "Sütunu hizala", + "clearContents": "İçeriği temizle", + "headerRow": "Başlık satırı", + "columnActions": "Sütun işlemleri", + "rowActions": "Satır işlemleri", + "deleteRow": "Satırı sil", + "deleteColumn": "Sütunu sil", + "deleteTable": "Tabloyu sil", + "richEditor": "Zengin düzenleyici", + "rawSource": "Ham kaynak", + "writeSomething": "Bir şeyler yazın…", + "apply": "Uygula", + "remove": "Kaldır" + }, + "overwrite": { + "title": "Mevcut öğenin üzerine yazılsın mı?", + "description": "Bu adla bir öğe zaten mevcut. Üzerine yazmak ister misiniz?", + "overwrite": "Üzerine yaz", + "keepBoth": "İkisini de tut", + "skip": "Atla", + "replaceTitle": "Mevcut öğe değiştirilsin mi?", + "replace": "Değiştir", + "replaceAll": "Tümünü değiştir", + "singleConflictDescription": "\"{{name}}\" adlı bir öğe zaten /{{destination}} konumunda mevcut. Değiştirmek ister misiniz?", + "batchConflictDescription": "Hedefte {{count}} öğe zaten mevcut. Hepsini değiştirmek ister misiniz?" + }, + "detailNavigation": { + "close": "Kapat", + "previous": "Önceki", + "next": "Sonraki", + "searchPlaceholder": "Ara…", + "clearSearch": "Aramayı temizle", + "openList": "{{title}} listesini aç ({{position}})", + "showAllMatching": "Eşleşen tüm {{title}} göster", + "noItemsMatchQuery": "\"{{query}}\" ile eşleşen öğe yok.", + "noItemsMatchFilter": "Geçerli filtreyle eşleşen öğe yok." + } +} diff --git a/frontend/src/pages/dashboard/dashboard-analytics.tsx b/frontend/src/pages/dashboard/dashboard-analytics.tsx index 652e075f4..d89dc731d 100644 --- a/frontend/src/pages/dashboard/dashboard-analytics.tsx +++ b/frontend/src/pages/dashboard/dashboard-analytics.tsx @@ -20,6 +20,7 @@ import { ToolcallsStatsByPeriodDocument, UsageStatsByPeriodDocument, } from '@/graphql/types'; +import { useI18n } from '@/hooks/use-i18n'; import { cn } from '@/lib/utils'; import { formatCost, formatDuration, formatNumber, formatTokenCount } from '@/lib/utils/format'; @@ -62,6 +63,7 @@ type FlowExecution = { }; export function DashboardAnalytics({ period }: { period: UsageStatsPeriod }) { + const { t } = useI18n(); const { data: usageByPeriodData, error: usageByPeriodError, @@ -152,12 +154,12 @@ export function DashboardAnalytics({ period }: { period: UsageStatsPeriod }) { return (
@@ -212,11 +214,11 @@ export function DashboardAnalytics({ period }: { period: UsageStatsPeriod }) {
@@ -307,7 +309,7 @@ export function DashboardAnalytics({ period }: { period: UsageStatsPeriod }) { dataKey="tokensOut" fill={CHART_COLORS.area2} fillOpacity={0.3} - name="Tokens Out" + name={t('dashboard.tokensOutLegend')} stroke={CHART_COLORS.area2} type="monotone" /> @@ -316,12 +318,12 @@ export function DashboardAnalytics({ period }: { period: UsageStatsPeriod }) {
@@ -366,7 +368,7 @@ export function DashboardAnalytics({ period }: { period: UsageStatsPeriod }) { dataKey="costOut" fill={CHART_COLORS.area3} fillOpacity={0.3} - name="Cost Out" + name={t('dashboard.costOutLegend')} stroke={CHART_COLORS.area3} type="monotone" /> @@ -375,8 +377,8 @@ export function DashboardAnalytics({ period }: { period: UsageStatsPeriod }) { - Flow Execution Details - Execution time and tool calls breakdown per flow + {t('dashboard.flowExecutionDetails')} + {t('dashboard.flowExecutionDetailsDesc')} {executionStatsLoading ? ( @@ -390,7 +392,7 @@ export function DashboardAnalytics({ period }: { period: UsageStatsPeriod }) { ) : !deferredExecutionStats.length ? (

- No flow executions in this period + {t('dashboard.noFlowExecutions')}

) : (
sum + task.subtasks.length, 0); @@ -438,15 +441,18 @@ const FlowExecutionItem = memo(function FlowExecutionItem({
- {flow.flowTitle || `Flow #${flow.flowId}`} + + {flow.flowTitle || t('dashboard.flowNumber', { id: flow.flowId })} + {flowMeta?.status && } {flowMeta?.provider?.name && {flowMeta.provider.name}}
- {taskCount} {taskCount === 1 ? 'task' : 'tasks'} - {subtaskCount > 0 && ` · ${subtaskCount} ${subtaskCount === 1 ? 'subtask' : 'subtasks'}`} + {taskCount} {taskCount === 1 ? t('dashboard.taskSingular') : t('dashboard.taskPlural')} + {subtaskCount > 0 && + ` · ${subtaskCount} ${subtaskCount === 1 ? t('dashboard.subtaskSingular') : t('dashboard.subtaskPlural')}`} {flow.totalAssistantsCount > 0 && - ` · ${flow.totalAssistantsCount} ${flow.totalAssistantsCount === 1 ? 'assistant' : 'assistants'}`} + ` · ${flow.totalAssistantsCount} ${flow.totalAssistantsCount === 1 ? t('dashboard.assistantSingular') : t('dashboard.assistantPlural')}`}
@@ -475,6 +481,7 @@ const FlowExecutionItem = memo(function FlowExecutionItem({ }); const TaskExecutionItem = memo(function TaskExecutionItem({ task }: { task: FlowExecution['tasks'][number] }) { + const { t } = useI18n(); const [isOpen, setIsOpen] = useState(false); const hasSubtasks = task.subtasks.length > 0; @@ -492,7 +499,9 @@ const TaskExecutionItem = memo(function TaskExecutionItem({ task }: { task: Flow ) : ( )} -
{task.taskTitle || `Task #${task.taskId}`}
+
+ {task.taskTitle || t('dashboard.taskNumber', { id: task.taskId })} +
@@ -513,7 +522,7 @@ const TaskExecutionItem = memo(function TaskExecutionItem({ task }: { task: Flow key={subtask.subtaskId} >
- {subtask.subtaskTitle || `Subtask #${subtask.subtaskId}`} + {subtask.subtaskTitle || t('dashboard.subtaskNumber', { id: subtask.subtaskId })}
diff --git a/frontend/src/pages/dashboard/dashboard-overview.tsx b/frontend/src/pages/dashboard/dashboard-overview.tsx index 1eb0d8a66..81478ad42 100644 --- a/frontend/src/pages/dashboard/dashboard-overview.tsx +++ b/frontend/src/pages/dashboard/dashboard-overview.tsx @@ -18,9 +18,11 @@ import { UsageStatsByProviderDocument, UsageStatsTotalDocument, } from '@/graphql/types'; +import { useI18n } from '@/hooks/use-i18n'; import { formatCost, formatDuration, formatNumber, formatTokenCount } from '@/lib/utils/format'; export function DashboardOverview() { + const { t } = useI18n(); const { data: usageTotalData, error: usageTotalError, @@ -85,43 +87,49 @@ export function DashboardOverview() {
} loading={flowsTotalLoading} - title="Total Flows" + title={t('dashboard.totalFlows')} value={flowsTotal ? formatNumber(flowsTotal.totalFlowsCount) : '0'} /> } loading={toolcallsTotalLoading} - title="Tool Calls" + title={t('dashboard.toolCalls')} value={toolcallsTotal ? formatNumber(toolcallsTotal.totalCount) : '0'} /> } loading={usageTotalLoading} - title="Total Tokens" + title={t('dashboard.totalTokens')} value={formatTokenCount(totalTokens)} /> } loading={usageTotalLoading} - title="Total Cost" + title={t('dashboard.totalCost')} value={formatCost(totalCost)} />
- Usage by Provider - LLM token usage and costs grouped by provider + {t('dashboard.usageByProvider')} + {t('dashboard.usageByProviderDesc')} {usageByProviderLoading ? ( @@ -136,8 +144,8 @@ export function DashboardOverview() { - Usage by Model - LLM token usage and costs grouped by model + {t('dashboard.usageByModel')} + {t('dashboard.usageByModelDesc')} {usageByModelLoading ? ( @@ -152,8 +160,8 @@ export function DashboardOverview() { - Usage by Agent Type - LLM token usage and costs grouped by agent type + {t('dashboard.usageByAgentType')} + {t('dashboard.usageByAgentTypeDesc')} {usageByAgentTypeLoading ? ( @@ -168,8 +176,8 @@ export function DashboardOverview() { - Tool Calls by Function - Execution statistics for each tool function + {t('dashboard.toolCallsByFunction')} + {t('dashboard.toolCallsByFunctionDesc')} {toolcallsByFunctionLoading ? ( @@ -180,11 +188,17 @@ export function DashboardOverview() { - Function - Type - Count - Total Duration - Avg Duration + {t('dashboard.function')} + {t('dashboard.type')} + + {t('dashboard.count')} + + + {t('dashboard.totalDurationCol')} + + + {t('dashboard.avgDuration')} + @@ -193,7 +207,7 @@ export function DashboardOverview() { {item.functionName} - {item.isAgent ? 'Agent' : 'Tool'} + {item.isAgent ? t('dashboard.agentBadge') : t('dashboard.toolBadge')} {formatNumber(item.totalCount)} @@ -247,18 +261,20 @@ function UsageStatsRow({ label, stats }: { label: string; stats: UsageStatsFragm } function UsageStatsTable({ rows }: { rows: Array<{ label: string; stats: UsageStatsFragmentFragment }> }) { + const { t } = useI18n(); + return (
- Name - Tokens In - Tokens Out - Cache In - Cache Out - Cost In - Cost Out - Total Cost + {t('dashboard.name')} + {t('dashboard.tokensIn')} + {t('dashboard.tokensOut')} + {t('dashboard.cacheIn')} + {t('dashboard.cacheOut')} + {t('dashboard.costIn')} + {t('dashboard.costOut')} + {t('dashboard.totalCostCol')} diff --git a/frontend/src/pages/dashboard/dashboard.tsx b/frontend/src/pages/dashboard/dashboard.tsx index e3e3fa33e..1a818a2b4 100644 --- a/frontend/src/pages/dashboard/dashboard.tsx +++ b/frontend/src/pages/dashboard/dashboard.tsx @@ -4,17 +4,12 @@ import { useState, useTransition } from 'react'; import { AppHeader, AppHeaderContent, AppHeaderTitle } from '@/components/layouts/app/app-header'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { UsageStatsPeriod } from '@/graphql/types'; +import { useI18n } from '@/hooks/use-i18n'; import { usePageStorageKeys } from '@/hooks/use-page-storage-keys'; import { cn } from '@/lib/utils'; import { DashboardAnalytics } from '@/pages/dashboard/dashboard-analytics'; import { DashboardOverview } from '@/pages/dashboard/dashboard-overview'; -const periodOptions: { label: string; value: UsageStatsPeriod }[] = [ - { label: 'Week', value: UsageStatsPeriod.Week }, - { label: 'Month', value: UsageStatsPeriod.Month }, - { label: 'Quarter', value: UsageStatsPeriod.Quarter }, -]; - const VALID_PERIODS = new Set(Object.values(UsageStatsPeriod)); const loadPeriod = (storageKey: string): UsageStatsPeriod => { @@ -40,6 +35,12 @@ const savePeriod = (storageKey: string, value: UsageStatsPeriod): void => { }; function Dashboard() { + const { t } = useI18n(); + const periodOptions: { label: string; value: UsageStatsPeriod }[] = [ + { label: t('dashboard.week'), value: UsageStatsPeriod.Week }, + { label: t('dashboard.month'), value: UsageStatsPeriod.Month }, + { label: t('dashboard.quarter'), value: UsageStatsPeriod.Quarter }, + ]; const { period: periodStorageKey } = usePageStorageKeys(); const [activeTab, setActiveTab] = useState('analytics'); const [period, setPeriod] = useState(() => loadPeriod(periodStorageKey)); @@ -74,7 +75,9 @@ function Dashboard() { <> - }>Dashboard + }> + {t('common.dashboard')} + @@ -86,8 +89,8 @@ function Dashboard() { >
- Analytics - Overview + {t('dashboard.analytics')} + {t('dashboard.overview')} {activeTab === 'analytics' && ( diff --git a/frontend/src/pages/flows/flow-report.tsx b/frontend/src/pages/flows/flow-report.tsx index 97ed02087..933c4bed8 100644 --- a/frontend/src/pages/flows/flow-report.tsx +++ b/frontend/src/pages/flows/flow-report.tsx @@ -5,6 +5,7 @@ import { useParams, useSearchParams } from 'react-router-dom'; import Logo from '@/components/icons/logo'; import Markdown from '@/components/shared/markdown'; import { FlowReportDocument } from '@/graphql/types'; +import { useI18n } from '@/hooks/use-i18n'; import { Log } from '@/lib/log'; import { generateFileName, generatePDFFromMarkdown, generateReport } from '@/lib/report'; @@ -12,6 +13,7 @@ type PdfPhase = 'done' | 'error' | 'idle'; type ReportState = 'content' | 'error' | 'generating' | 'loading'; function FlowReport() { + const { t } = useI18n(); const { flowId } = useParams<{ flowId: string }>(); const [searchParams] = useSearchParams(); const download = searchParams.has('download'); @@ -66,10 +68,10 @@ function FlowReport() { }) .catch((err) => { Log.error('PDF generation failed:', err); - setPdfError('Failed to generate PDF'); + setPdfError(t('flow.failedToGeneratePdf')); setPdfPhase('error'); }); - }, [dataReady, download, silent, reportContent, data]); + }, [dataReady, download, silent, reportContent, data, t]); let state: ReportState; let errorMessage: null | string = null; @@ -78,7 +80,7 @@ function FlowReport() { state = 'loading'; } else if (!data?.flow) { state = 'error'; - errorMessage = 'Failed to load flow data'; + errorMessage = t('flow.failedToLoadFlowData'); } else if (pdfPhase === 'error') { state = 'error'; errorMessage = pdfError; @@ -95,13 +97,11 @@ function FlowReport() {

- {state === 'loading' ? 'Loading Report...' : 'Generating PDF...'} + {state === 'loading' ? t('flow.loadingReport') : t('flow.generatingPdf')}

- {state === 'loading' - ? 'Please wait while we prepare your penetration testing report.' - : 'Creating your PDF document. This may take a few moments.'} + {state === 'loading' ? t('flow.preparingReportDesc') : t('flow.creatingPdfDesc')}

@@ -115,15 +115,17 @@ function FlowReport() {
-

Error Loading Report

+

+ {t('flow.errorLoadingReport')} +

- {errorMessage || 'An unexpected error occurred while loading the report.'} + {errorMessage || t('flow.unexpectedReportError')}

diff --git a/frontend/src/pages/flows/flow.tsx b/frontend/src/pages/flows/flow.tsx index 52fb758ff..4e20df08a 100644 --- a/frontend/src/pages/flows/flow.tsx +++ b/frontend/src/pages/flows/flow.tsx @@ -49,6 +49,7 @@ import { useFlowDetailNavigation } from '@/features/flows/use-flow-detail-naviga import { RenameFlowDocument, ResultType, StatusType } from '@/graphql/types'; import { useBreakpoint } from '@/hooks/use-breakpoint'; import { useFlowTabDetection } from '@/hooks/use-flow-tab-detection'; +import { useI18n } from '@/hooks/use-i18n'; import { Log } from '@/lib/log'; import { copyToClipboard, downloadTextFile, generateFileName, generateReport } from '@/lib/report'; import { routes } from '@/lib/routes'; @@ -58,14 +59,14 @@ import { useFavorites } from '@/providers/favorites-provider'; import { useFlow } from '@/providers/flow-provider'; import { type Flow as FlowItem, useFlows } from '@/providers/flows-provider'; -const renderFlowItem = (item: FlowItem, isCurrent: boolean): ReactNode => ( +const renderFlowItem = (item: FlowItem, isCurrent: boolean, t: (key: string, opts?: Record) => string): ReactNode => ( <> - {item.title || `Flow #${item.id}`} + {item.title || t('flow.flowNumber', { id: item.id })} ( function Flow() { const { isDesktop, isMobile } = useBreakpoint(); const navigate = useNavigate(); + const { t } = useI18n(); const { flowData, flowId, flowLoadError, isFlowMissing, isLoading: isFlowLoading, refetchFlow } = useFlow(); const { deleteFlow, finishFlow } = useFlows(); @@ -129,15 +131,15 @@ function Flow() { }); if (data?.renameFlow === ResultType.Success) { - toast.success('Flow renamed successfully'); + toast.success(t('flow.renameSuccess')); handleFlowRenameCancel(); } } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Failed to rename flow'; + const errorMessage = error instanceof Error ? error.message : t('flow.renameFailed'); toast.error(errorMessage); } }); - }, [editingInputRef, flowId, handleFlowRenameCancel, renameFlowMutation, setOptimisticFlowTitle]); + }, [editingInputRef, flowId, handleFlowRenameCancel, renameFlowMutation, setOptimisticFlowTitle, t]); const handleFlowFinish = useCallback(async () => { if (!flow) { @@ -186,7 +188,7 @@ function Flow() { - Flow + {t('flow.flowBreadcrumb')} @@ -196,7 +198,7 @@ function Flow() {
@@ -242,7 +244,7 @@ function Flow() { inputRef={editingInputRef} onCancel={handleFlowRenameCancel} onSave={handleFlowRenameSave} - placeholder="Flow title" + placeholder={t('flow.flowTitle')} /> ) : flow ? ( @@ -251,14 +253,14 @@ function Flow() { className="max-w-64 min-w-0 cursor-text truncate select-none" onDoubleClick={handleFlowRenameStart} > - {flowTitle || 'Select a flow'} + {flowTitle || t('flow.selectFlow')} - Double-click to rename + {t('flow.doubleClickRename')} ) : ( - {flowTitle || 'Select a flow'} + {flowTitle || t('flow.selectFlow')} )} @@ -270,14 +272,14 @@ function Flow() { {!isMobile && ( controller={flowNav} - renderItem={renderFlowItem} + renderItem={(item, isCurrent) => renderFlowItem(item, isCurrent, t)} sheetIcon={} - sheetTitle="Flows" + sheetTitle={t('common.flows')} /> )} {flowId && !isMobile && ( @@ -653,7 +657,7 @@ function Flows() { columns={columns} data={flows} empty={{ entityName: 'flows' }} - filterPlaceholder="Filter flows..." + filterPlaceholder={t('flow.filterFlows')} filterValue={filter} isVirtualized onFilterChange={setFilter} @@ -664,13 +668,13 @@ function Flows() { /> diff --git a/frontend/src/pages/flows/new-flow.tsx b/frontend/src/pages/flows/new-flow.tsx index 4c64b2102..c7abaf3f8 100644 --- a/frontend/src/pages/flows/new-flow.tsx +++ b/frontend/src/pages/flows/new-flow.tsx @@ -5,6 +5,7 @@ import { AppHeader, AppHeaderContent, AppHeaderTitle } from '@/components/layout import { Card, CardContent } from '@/components/ui/card'; import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { FlowForm, type FlowFormValues } from '@/features/flows/flow-form'; +import { useI18n } from '@/hooks/use-i18n'; import { routes } from '@/lib/routes'; import { useFlows } from '@/providers/flows-provider'; import { useProviders } from '@/providers/providers-provider'; @@ -12,6 +13,7 @@ import { useSystemSettings } from '@/providers/system-settings-provider'; function NewFlow() { const navigate = useNavigate(); + const { t } = useI18n(); const { selectedProvider } = useProviders(); const { createFlow, createFlowWithAssistant } = useFlows(); @@ -46,15 +48,15 @@ function NewFlow() { <> - New flow + {t('flow.newFlow')}
-

Create a new flow

-

Describe what you would like PentAGI to test

+

{t('flow.createFlow')}

+

{t('flow.describeFlow')}

setFlowType(value as 'assistant' | 'automation')} @@ -65,13 +67,13 @@ function NewFlow() { disabled={isLoading} value="automation" > - Automation + {t('flow.automation')} - Assistant + {t('flow.assistant')} @@ -85,9 +87,9 @@ function NewFlow() { placeholder={ !isLoading ? flowType === 'automation' - ? 'Describe what you would like PentAGI to test...' - : 'What would you like me to help you with?' - : 'Creating a new flow...' + ? t('flow.describeAutomation') + : t('flow.describeAssistant') + : t('flow.creatingFlow') } type={flowType} /> diff --git a/frontend/src/pages/knowledges/knowledge.tsx b/frontend/src/pages/knowledges/knowledge.tsx index 01a2d7a24..b1cb82fd7 100644 --- a/frontend/src/pages/knowledges/knowledge.tsx +++ b/frontend/src/pages/knowledges/knowledge.tsx @@ -17,11 +17,13 @@ import { } from '@/features/knowledges/knowledge-form'; import { KnowledgeLayout } from '@/features/knowledges/knowledge-layout'; import { KnowledgeDocumentDocument } from '@/graphql/types'; +import { useI18n } from '@/hooks/use-i18n'; import { isNotFoundError } from '@/lib/errors'; import { routes } from '@/lib/routes'; import { useKnowledges } from '@/providers/knowledges-provider'; function Knowledge() { + const { t } = useI18n(); const navigate = useNavigate(); const { knowledgeId } = useParams<{ knowledgeId?: string }>(); const { createKnowledge, updateKnowledge } = useKnowledges(); @@ -52,10 +54,10 @@ function Knowledge() { } if (!knowledge) { - toast.error('Knowledge document not found'); + toast.error(t('knowledges.notFound')); navigate(routes.knowledges, { replace: true }); } - }, [isNew, isLoadingKnowledge, knowledge, loadError, navigate]); + }, [isNew, isLoadingKnowledge, knowledge, loadError, navigate, t]); const initialValues = useMemo( () => (knowledge ? documentToFormValues(knowledge) : newDocumentDefaults), @@ -99,7 +101,7 @@ function Knowledge() { refetch()} - title="Error loading knowledge document" + title={t('knowledges.errorLoadingKnowledgeDoc')} />
diff --git a/frontend/src/pages/knowledges/knowledges.tsx b/frontend/src/pages/knowledges/knowledges.tsx index fdbe50a23..99f89e3b2 100644 --- a/frontend/src/pages/knowledges/knowledges.tsx +++ b/frontend/src/pages/knowledges/knowledges.tsx @@ -33,6 +33,7 @@ import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTi import { InputSearch } from '@/components/ui/input-search'; import { Spinner } from '@/components/ui/spinner'; import { KnowledgeDocType } from '@/graphql/types'; +import { useI18n } from '@/hooks/use-i18n'; import { useTableState } from '@/hooks/use-table-state'; import { routes } from '@/lib/routes'; import { mergeHrefWithSearchParams, URL_PARAMS } from '@/lib/url-params'; @@ -61,6 +62,7 @@ const docTypeSubtype = (k: Knowledge): null | string => { }; function Knowledges() { + const { t } = useI18n(); const navigate = useNavigate(); const location = useLocation(); const { deleteKnowledge, error, isLoading, knowledges, refetch, renameKnowledge } = useKnowledges(); @@ -149,14 +151,14 @@ function Knowledges() { try { await renameKnowledge(editingKnowledgeId, newQuestion); - toast.success('Knowledge renamed successfully'); + toast.success(t('knowledges.renamedSuccess')); setEditingKnowledgeId(null); } catch { // Error already handled in provider with toast } finally { setIsRenameLoading(false); } - }, [editingKnowledgeId, knowledges, renameKnowledge]); + }, [editingKnowledgeId, knowledges, renameKnowledge, t]); const handleDelete = async () => { if (!deletingKnowledge) { @@ -209,11 +211,11 @@ function Knowledges() { header: ({ column }) => ( ), maxSize: 180, - meta: { columnMenuLabel: 'Type', searchable: true }, + meta: { columnMenuLabel: t('knowledges.typeColumn'), searchable: true }, minSize: 110, size: 130, }, @@ -234,7 +236,7 @@ function Knowledges() { inputRef={editingInputRef} onCancel={handleKnowledgeRenameCancel} onSave={handleKnowledgeRenameSave} - placeholder="Knowledge question" + placeholder={t('knowledges.questionPlaceholder')} /> ); @@ -252,10 +254,10 @@ function Knowledges() { header: ({ column }) => ( ), - meta: { columnMenuLabel: 'Question', searchable: true }, + meta: { columnMenuLabel: t('knowledges.questionColumn'), searchable: true }, minSize: 180, size: 280, }, @@ -270,14 +272,14 @@ function Knowledges() { className="shrink-0 whitespace-nowrap" variant="outline" > - flow #{k.flowId} + {t('knowledges.flowBadge', { id: k.flowId })} ) : null} - {k.manual ? 'manual' : 'agent'} + {k.manual ? t('knowledges.manualBadge') : t('knowledges.agentBadge')} ); @@ -285,12 +287,12 @@ function Knowledges() { enableSorting: false, header: () => ( - Flags + {t('knowledges.flagsColumn')} ), id: 'flags', maxSize: 200, - meta: { columnMenuLabel: 'Flags' }, + meta: { columnMenuLabel: t('knowledges.flagsColumn') }, minSize: 110, size: 150, }, @@ -303,7 +305,7 @@ function Knowledges() { @@ -470,7 +472,7 @@ function Knowledges() { columns={columns} data={knowledges} empty={{ entityName: 'knowledge documents' }} - filterPlaceholder="Filter knowledge documents..." + filterPlaceholder={t('knowledges.filterKnowledges')} filterValue={filter} onFilterChange={setFilter} onRowClick={(k) => { @@ -482,8 +484,8 @@ function Knowledges() { /> diff --git a/frontend/src/pages/resources/resources.tsx b/frontend/src/pages/resources/resources.tsx index 7f0cc957f..67d3551f9 100644 --- a/frontend/src/pages/resources/resources.tsx +++ b/frontend/src/pages/resources/resources.tsx @@ -54,6 +54,7 @@ import { useResourcesSearch } from '@/features/resources/use-resources-search'; import { useResourcesUpload } from '@/features/resources/use-resources-upload'; import { useEffectAfterMount } from '@/hooks/use-effect-after-mount'; import { useFilesDragAndDrop } from '@/hooks/use-files-drag-and-drop'; +import { useI18n } from '@/hooks/use-i18n'; import { usePageStorageKeys } from '@/hooks/use-page-storage-keys'; import { copyToClipboard } from '@/lib/report'; import { migrateLegacyViewOptions, saveViewOptions } from '@/lib/view-options-storage'; @@ -103,6 +104,7 @@ const seedViewOptions = (storageKey: string): ResourcesViewOptions => { }; function Resources() { + const { t } = useI18n(); const { error, isInitialLoading, refetch, resources } = useResources(); const search = useResourcesSearch(); @@ -220,35 +222,41 @@ function Resources() { [dndMoveAction], ); - const handleCopyPath = useCallback(async (file: FileNode) => { - const wasCopied = await copyToClipboard(file.path); + const handleCopyPath = useCallback( + async (file: FileNode) => { + const wasCopied = await copyToClipboard(file.path); - if (wasCopied) { - toast.success('Path copied to clipboard'); + if (wasCopied) { + toast.success(t('resources.pathCopied')); - return; - } + return; + } - toast.error('Failed to copy path'); - }, []); + toast.error(t('resources.copyPathFailed')); + }, + [t], + ); // Join the selected paths with `\n` so the result pastes as a clean // newline-separated list into the agent chat, a shell command, or notes. - const handleBulkCopyPaths = useCallback(async (paths: string[]) => { - if (paths.length === 0) { - return; - } + const handleBulkCopyPaths = useCallback( + async (paths: string[]) => { + if (paths.length === 0) { + return; + } - const wasCopied = await copyToClipboard(paths.join('\n')); + const wasCopied = await copyToClipboard(paths.join('\n')); - if (wasCopied) { - toast.success(`${paths.length} ${pluralizeItems(paths.length)} copied to clipboard`); + if (wasCopied) { + toast.success(t('resources.itemsCopied', { count: paths.length, items: pluralizeItems(paths.length) })); - return; - } + return; + } - toast.error('Failed to copy paths'); - }, []); + toast.error(t('resources.copyPathsFailed')); + }, + [t], + ); /** * "Open" gesture — fires on double-click or Enter for a file row. @@ -304,7 +312,7 @@ function Resources() { appliesToFiles: false, icon: FolderPlus, id: 'resources-mkdir-here', - label: 'New folder', + label: t('resources.newFolder'), onSelect: handleMkdirHere, separatorBefore: true, }, @@ -313,14 +321,14 @@ function Resources() { appliesToFiles: false, icon: Upload, id: 'resources-upload-here', - label: 'Upload files', + label: t('resources.uploadFiles'), onSelect: handleUploadHere, }, { appliesToDirs: true, icon: FileSymlink, id: 'resources-rename', - label: 'Rename or move', + label: t('resources.renameOrMove'), onSelect: (file) => setFilesToMove([file]), separatorBefore: true, }, @@ -328,12 +336,12 @@ function Resources() { appliesToDirs: true, icon: Copy, id: 'resources-copy', - label: 'Copy to…', + label: t('resources.copyTo'), onSelect: (file) => setFilesToCopy([file]), }, deleteAction(deletion.requestDelete), ], - [deletion.requestDelete, handleCopyPath, handleMkdirHere, handleUploadHere], + [deletion.requestDelete, handleCopyPath, handleMkdirHere, handleUploadHere, t], ); // Bulk-action set, rendered in the bulk-actions bar when at least one row @@ -358,17 +366,17 @@ function Resources() { { icon: FolderPlus, id: 'resources-empty-mkdir', - label: 'New folder', + label: t('resources.newFolder'), onSelect: () => setIsMkdirOpen(true), }, { icon: Upload, id: 'resources-empty-upload', - label: 'Upload files', + label: t('resources.uploadFiles'), onSelect: upload.openFilePicker, }, ], - [upload.openFilePicker], + [upload.openFilePicker, t], ); const handleDeleteDialogOpenChange = useCallback( @@ -383,21 +391,21 @@ function Resources() { const pageHeader = ( - }>Resources + }>{t('resources.title')} } - label="New folder" + label={t('resources.newFolder')} onClick={() => setIsMkdirOpen(true)} variant="outline" /> : } - label={upload.isUploading ? 'Uploading...' : 'Upload files'} + label={upload.isUploading ? t('resources.uploading') : t('resources.uploadFiles')} onClick={upload.openFilePicker} variant="secondary" /> @@ -409,13 +417,13 @@ function Resources() { const noResourcesState = ( ); @@ -425,9 +433,11 @@ function Resources() { - No matches + {t('resources.noMatchesTitle')} - No resources match {search.debouncedQuery.trim()}. Try a different query. + {t('resources.noMatchesDesc', { + query: search.debouncedQuery.trim(), + })} @@ -442,7 +452,7 @@ function Resources() { @@ -471,7 +481,7 @@ function Resources() {
- Drop files to upload + {t('resources.dropFilesToUpload')}
)} @@ -479,17 +489,17 @@ function Resources() {
search.setQuery(event.target.value)} - placeholder="Search resources..." + placeholder={t('resources.searchPlaceholder')} type="text" value={search.rawQuery} /> {search.rawQuery ? ( @@ -501,7 +511,7 @@ function Resources() {
diff --git a/frontend/src/pages/settings/settings-account.tsx b/frontend/src/pages/settings/settings-account.tsx index b42ebe739..3a8c8a064 100644 --- a/frontend/src/pages/settings/settings-account.tsx +++ b/frontend/src/pages/settings/settings-account.tsx @@ -1,7 +1,8 @@ import { format } from 'date-fns'; -import { enUS } from 'date-fns/locale'; +import { enUS, tr } from 'date-fns/locale'; import { Lock, Mail, User } from 'lucide-react'; import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { AppHeader, AppHeaderContent, AppHeaderTitle } from '@/components/layouts/app/app-header'; import { Badge } from '@/components/ui/badge'; @@ -20,6 +21,7 @@ const PROVIDER_LABELS: Record = { }; function SettingsAccount() { + const { t, i18n } = useTranslation(); const { authInfo } = useUser(); const user = authInfo?.user; const [editingSections, setEditingSections] = useState>(new Set()); @@ -41,19 +43,22 @@ function SettingsAccount() { const displayName = user.name?.trim() || user.mail; const initial = ([...(displayName || '?')][0] ?? '?').toUpperCase(); const createdAt = user.created_at ? new Date(user.created_at) : null; + const dateLocale = i18n.language === 'tr' ? tr : enUS; const memberSince = - createdAt && !Number.isNaN(createdAt.getTime()) ? format(createdAt, 'MMMM yyyy', { locale: enUS }) : null; + createdAt && !Number.isNaN(createdAt.getTime()) + ? t('account.memberSince', { date: format(createdAt, 'MMMM yyyy', { locale: dateLocale }) }) + : null; const accountLabel = isLocal - ? 'Local account' + ? t('account.localAccount') : user.provider ? (PROVIDER_LABELS[user.provider] ?? user.provider) - : 'OAuth account'; + : t('account.oauthAccount'); return ( <> - }>Account + }>{t('account.title')}
@@ -65,7 +70,7 @@ function SettingsAccount() {
{displayName} {memberSince && ( - Member since {memberSince} + {memberSince} )}
- Display name - The name shown across the app. + {t('account.displayName')} + {t('account.displayNameDesc')}
{!editingSections.has('name') && ( )}
@@ -111,9 +116,11 @@ function SettingsAccount() {
- Email address + {t('account.emailAddress')} - {isLocal ? 'The email you use to sign in.' : `Linked from your ${accountLabel}.`} + {isLocal + ? t('account.emailSignIn') + : t('account.emailLinkedFrom', { provider: accountLabel })}
{isLocal && !editingSections.has('email') && ( @@ -122,7 +129,7 @@ function SettingsAccount() { size="sm" variant="outline" > - Change + {t('account.change')} )}
@@ -145,8 +152,8 @@ function SettingsAccount() {
- Password - Change your account password. + {t('account.password')} + {t('account.changePassword')}
{!editingSections.has('password') && ( )}
diff --git a/frontend/src/pages/settings/settings-api-tokens.tsx b/frontend/src/pages/settings/settings-api-tokens.tsx index 680a293d2..2a56edb8b 100644 --- a/frontend/src/pages/settings/settings-api-tokens.tsx +++ b/frontend/src/pages/settings/settings-api-tokens.tsx @@ -2,8 +2,9 @@ import type { ColumnDef } from '@tanstack/react-table'; import { useMutation, useQuery, useSubscription } from '@apollo/client/react'; import { format } from 'date-fns'; -import { enUS } from 'date-fns/locale'; +import { enUS, tr as trLocale } from 'date-fns/locale'; import { CalendarIcon, Check, Copy, Ellipsis, ExternalLink, Key, Pencil, Plus, Trash, X } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; import { useCallback, useId, useMemo, useState } from 'react'; import { type Control, Controller, useFormState } from 'react-hook-form'; import { toast } from 'sonner'; @@ -101,19 +102,20 @@ const getTokenExpirationDate = (token: APIToken): Date => { const getStatusDisplay = ( token: APIToken, + t: (key: string) => string, ): { label: string; variant: 'default' | 'destructive' | 'outline' | 'secondary' } => { const expired = isTokenExpired(token); if (expired) { - return { label: 'expired', variant: 'destructive' }; + return { label: t('apiTokens.expired'), variant: 'destructive' }; } if (token.status === 'active') { - return { label: 'active', variant: 'default' }; + return { label: t('apiTokens.active'), variant: 'default' }; } if (token.status === 'revoked') { - return { label: 'revoked', variant: 'outline' }; + return { label: t('apiTokens.revoked'), variant: 'outline' }; } return { label: token.status, variant: 'secondary' }; @@ -166,11 +168,12 @@ function CreateRowActions({ onSubmit: () => void; }) { const { isValid } = useFormState({ control }); + const { t } = useTranslation(); return (
@@ -629,7 +640,7 @@ function SettingsAPITokens() { header: ({ column }) => ( ), size: 150, @@ -647,7 +658,7 @@ function SettingsAPITokens() { const isCreating = token.id === 'create-new'; if (isCreating) { - return
N/A
; + return
{t('common.notAvailable')}
; } const dateString = row.getValue('createdAt') as string; @@ -657,10 +668,10 @@ function SettingsAPITokens() { header: ({ column }) => ( ), - meta: { columnMenuLabel: 'Created' }, + meta: { columnMenuLabel: t('apiTokens.createdAt') }, size: 120, sortingFn: (rowA, rowB) => { const dateA = new Date(rowA.getValue('createdAt') as string); @@ -702,7 +713,7 @@ function SettingsAPITokens() {
@@ -896,10 +909,8 @@ function SettingsAPITokens() { - No API tokens configured - - Create your first API token to access PentAGI programmatically - + {t('apiTokens.noTokensTitle')} + {t('apiTokens.noTokensDesc')} @@ -923,8 +934,8 @@ function SettingsAPITokens() { columns={columns} data={creatingToken ? [createNewTokenPlaceholder, ...tokens] : tokens} - empty={{ entityName: 'API tokens' }} - filterPlaceholder="Filter tokens..." + empty={{ entityName: t('apiTokens.title') }} + filterPlaceholder={t('apiTokens.filterTokens')} filterValue={filter} onFilterChange={setFilter} onPageChange={handlePageChange} @@ -938,10 +949,8 @@ function SettingsAPITokens() { > - API Token Created - - Copy this token now. You won't be able to see it again for security reasons. - + {t('apiTokens.createTokenTitle')} + {t('apiTokens.createTokenDesc')}
{tokenSecret} @@ -954,16 +963,16 @@ function SettingsAPITokens() { const success = await copyToClipboard(tokenSecret); if (success) { - toast.success('Token copied to clipboard'); + toast.success(t('apiTokens.tokenCopied')); } else { - toast.error('Failed to copy token to clipboard'); + toast.error(t('apiTokens.tokenCopyFailed')); } } }} variant="secondary" > - Copy Token + {t('apiTokens.copyToken')}
handleDelete(deletingToken?.tokenId)} handleOpenChange={setIsDeleteDialogOpen} isOpen={isDeleteDialogOpen} itemName={deletingToken?.name || deletingToken?.tokenId} - itemType="token" + itemType={t('apiTokens.tokenNoun')} />
diff --git a/frontend/src/pages/settings/settings-prompt.tsx b/frontend/src/pages/settings/settings-prompt.tsx index a50a9b9bf..1972e1544 100644 --- a/frontend/src/pages/settings/settings-prompt.tsx +++ b/frontend/src/pages/settings/settings-prompt.tsx @@ -83,11 +83,10 @@ import { } from '@/graphql/types'; import { useAppForm } from '@/hooks/use-app-form'; import { useBreakpoint } from '@/hooks/use-breakpoint'; +import { useI18n } from '@/hooks/use-i18n'; import { composeRefs } from '@/lib/compose-refs'; import { formatPromptId } from '@/lib/route-titles/format-prompt-id'; -const VARIABLES_TITLE = 'Available variables'; - const systemFormSchema = z.object({ template: z.string().min(1, 'System template is required'), }); @@ -301,6 +300,7 @@ function SettingsPrompt() { function SettingsPromptEditor({ promptId }: { promptId?: string }) { const { isDesktop } = useBreakpoint(); + const { t } = useI18n(); const { data, error, loading, refetch } = useQuery(SettingsPromptsDocument); const [createPrompt, { loading: isCreateLoading }] = useMutation(CreatePromptDocument); @@ -358,7 +358,7 @@ function SettingsPromptEditor({ promptId }: { promptId?: string }) { setResetDialogOpen(false); } catch (error) { console.error('Reset error:', error); - setSubmitError(error instanceof Error ? error.message : 'An error occurred while resetting'); + setSubmitError(error instanceof Error ? error.message : t('prompts.resetFailed2')); setResetDialogOpen(false); } }; @@ -402,16 +402,32 @@ function SettingsPromptEditor({ promptId }: { promptId?: string }) { setValidationDialogOpen(true); } catch (error) { console.error('Validation error:', error); - setSubmitError(error instanceof Error ? error.message : 'An error occurred while validating'); + setSubmitError(error instanceof Error ? error.message : t('prompts.validateFailed')); } }; + const localizedSystemFormSchema = useMemo( + () => + z.object({ + template: z.string().min(1, t('prompts.systemTemplateRequired')), + }), + [t], + ); + + const localizedHumanFormSchema = useMemo( + () => + z.object({ + template: z.string().min(1, t('prompts.humanTemplateRequired')), + }), + [t], + ); + const systemForm = useAppForm({ defaultValues: { template: '', }, resetOptions: { keepDirtyValues: true }, - schema: systemFormSchema, + schema: localizedSystemFormSchema, }); const humanForm = useAppForm({ @@ -419,7 +435,7 @@ function SettingsPromptEditor({ promptId }: { promptId?: string }) { template: '', }, resetOptions: { keepDirtyValues: true }, - schema: humanFormSchema, + schema: localizedHumanFormSchema, }); const { isDirty: isSystemDirty, isValid: isSystemValid } = useFormState({ control: systemForm.control }); @@ -570,7 +586,7 @@ function SettingsPromptEditor({ promptId }: { promptId?: string }) { return true; } catch (error) { console.error('Submit error:', error); - setSubmitError(error instanceof Error ? error.message : 'An error occurred while saving'); + setSubmitError(error instanceof Error ? error.message : t('prompts.saveFailed')); return false; } @@ -595,7 +611,7 @@ function SettingsPromptEditor({ promptId }: { promptId?: string }) { const humanPromptType = agentData.human?.type; if (!humanPromptType) { - setSubmitError('Human prompt type not found'); + setSubmitError(t('prompts.humanPromptTypeNotFound')); return false; } @@ -621,7 +637,7 @@ function SettingsPromptEditor({ promptId }: { promptId?: string }) { return true; } catch (error) { console.error('Submit error:', error); - setSubmitError(error instanceof Error ? error.message : 'An error occurred while saving'); + setSubmitError(error instanceof Error ? error.message : t('prompts.saveFailed')); return false; } @@ -671,14 +687,14 @@ function SettingsPromptEditor({ promptId }: { promptId?: string }) { const pageHeader = ( - }>Edit Prompt + }>{t('prompts.editPrompt')} {promptInfo && ( : } - label={isValidateLoading ? 'Validating...' : 'Validate'} + label={isValidateLoading ? t('prompts.validating') : t('prompts.validatePrompt')} onClick={handleValidate} type="button" variant="outline" @@ -686,14 +702,14 @@ function SettingsPromptEditor({ promptId }: { promptId?: string }) { } - label="Save" + label={t('prompts.savePrompt')} loading={isLoading} type="submit" /> + )} @@ -1008,9 +1025,9 @@ function SettingsPromptEditor({ promptId }: { promptId?: string }) { - Diff + {t('prompts.diffTitle')} - Changes between current value and default template. + {t('prompts.diffDesc')}

- {VARIABLES_TITLE} + {t('prompts.availableVariables')}

- Click to insert at the cursor, or cycle through existing uses. + {t('prompts.variablesHint')}

{content}
@@ -1071,7 +1089,7 @@ function Variables({ currentTemplate, onVariableClick, variables }: VariablesPro variant="secondary" > - {VARIABLES_TITLE} + {t('prompts.availableVariables')} countVariableUses(currentTemplate, variables), [currentTemplate, variables]); @@ -1100,9 +1119,12 @@ function VariablesContent({ currentTemplate, onVariableClick, variables }: Varia {variables.map((variable) => { const count = counts[variable] ?? 0; const isUsed = count > 0; + const variableToken = `{{.${variable}}}`; const action = isUsed - ? `Go to next {{.${variable}}} in the template${count > 1 ? ` (${count} uses)` : ''}` - : `Insert {{.${variable}}} at the cursor`; + ? count > 1 + ? t('prompts.variableGoToNextCount', { count, variable: variableToken }) + : t('prompts.variableGoToNext', { variable: variableToken }) + : t('prompts.variableInsertAtCursor', { variable: variableToken }); return ( // className stays on Badge: Slot only concatenates, so `font-normal` would race diff --git a/frontend/src/pages/settings/settings-prompts.tsx b/frontend/src/pages/settings/settings-prompts.tsx index e5e519773..94d37e070 100644 --- a/frontend/src/pages/settings/settings-prompts.tsx +++ b/frontend/src/pages/settings/settings-prompts.tsx @@ -41,11 +41,15 @@ import { import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty'; import { Spinner } from '@/components/ui/spinner'; import { DeletePromptDocument, SettingsPromptsDocument } from '@/graphql/types'; +import { useI18n } from '@/hooks/use-i18n'; import { usePageStorageKeys } from '@/hooks/use-page-storage-keys'; import { routes } from '@/lib/routes'; const formatName = (key: string): string => key.replaceAll(/([A-Z])/g, ' $1').replace(/^./, (str) => str.toUpperCase()); +const statusKey = (status: 'Custom' | 'Default' | 'N/A'): 'common.custom' | 'common.default' | 'common.notAvailable' => + status === 'Custom' ? 'common.custom' : status === 'Default' ? 'common.default' : 'common.notAvailable'; + type AgentPromptTableData = { displayName: string; hasHuman: boolean; @@ -68,6 +72,7 @@ type ToolPromptTableData = { }; function SettingsPrompts() { + const { t } = useI18n(); const { data, error, loading: isLoading, refetch } = useQuery(SettingsPromptsDocument); const [deletePrompt, { loading: isDeleteLoading }] = useMutation(DeletePromptDocument); const navigate = useNavigate(); @@ -190,7 +195,7 @@ function SettingsPrompts() { setResetOperation(null); } catch (error) { - toast.error('Failed to reset prompt', { + toast.error(t('prompts.resetFailed'), { description: error instanceof Error ? error.message : undefined, }); } @@ -326,41 +331,41 @@ function SettingsPrompts() { onClick={() => handleColumnSort(column)} variant="link" > - Agent Name + {t('prompts.agentName')} {sorted === 'asc' ? : sorted === 'desc' ? : null} ); }, - meta: { columnMenuLabel: 'Agent Name', searchable: true }, + meta: { columnMenuLabel: t('prompts.agentName'), searchable: true }, }, { accessorKey: 'systemStatus', cell: ({ row }) => { - const status = row.getValue('systemStatus') as string; + const status = row.getValue('systemStatus') as 'Custom' | 'Default' | 'N/A'; return ( - {status} + {t(statusKey(status))} ); }, - header: 'System Prompt', - meta: { columnMenuLabel: 'System Prompt', searchable: true }, + header: t('prompts.systemPrompt'), + meta: { columnMenuLabel: t('prompts.systemPrompt'), searchable: true }, size: 100, }, { accessorKey: 'humanStatus', cell: ({ row }) => { - const status = row.getValue('humanStatus') as string; + const status = row.getValue('humanStatus') as 'Custom' | 'Default' | 'N/A'; return ( - {status} + {t(statusKey(status))} ); }, - header: 'Human Prompt', - meta: { columnMenuLabel: 'Human Prompt', searchable: true }, + header: t('prompts.humanPrompt'), + meta: { columnMenuLabel: t('prompts.humanPrompt'), searchable: true }, size: 100, }, { @@ -372,7 +377,7 @@ function SettingsPrompts() { ); }, - meta: { columnMenuLabel: 'Tool Name', searchable: true }, + meta: { columnMenuLabel: t('prompts.toolName'), searchable: true }, }, { accessorKey: 'status', cell: ({ row }) => { - const status = row.getValue('status') as string; + const status = row.getValue('status') as 'Custom' | 'Default' | 'N/A'; return ( - {status} + {t(statusKey(status))} ); }, - header: 'Prompt', - meta: { columnMenuLabel: 'Prompt', searchable: true }, + header: t('prompts.prompt'), + meta: { columnMenuLabel: t('prompts.prompt'), searchable: true }, size: 100, }, { @@ -533,7 +538,7 @@ function SettingsPrompts() { )}
@@ -392,6 +399,7 @@ function FormModelComboboxItem({ options, placeholder, }: FormModelComboboxItemProps) { + const { t } = useI18n(); const { field, fieldState } = useController({ control, defaultValue: undefined, @@ -410,7 +418,7 @@ function FormModelComboboxItem({ price?: null | { cacheRead: number; cacheWrite: number; input: number; output: number }, ): string => { if (!price || ((!price.input || price.input === 0) && (!price.output || price.output === 0))) { - return 'free'; + return t('providers.free'); } const formatValue = (value: number): string => { @@ -464,7 +472,7 @@ function FormModelComboboxItem({ @@ -481,13 +489,15 @@ function FormModelComboboxItem({
-

No {label.toLowerCase()} found.

+

+ {t('providers.noValueFound', { label: label.toLowerCase() })} +

{search && allowCustom && ( )}
@@ -608,56 +621,64 @@ const requiredString = (message: string) => .transform((value) => value ?? '') .pipe(z.string().min(1, message)); -const agentConfigSchema = z - .object({ - extraBody: optionalJsonObject, - frequencyPenalty: optionalNumber, - json: z.boolean().nullable().optional(), - maxLength: optionalNumber, - maxTokens: optionalNumber, - minLength: optionalNumber, - minP: optionalNumber, - model: requiredString('Model is required'), - n: optionalNumber, - presencePenalty: optionalNumber, - price: z - .object({ - cacheRead: optionalNumber, - cacheWrite: optionalNumber, - input: optionalNumber, - output: optionalNumber, - }) - .nullable() - .optional(), - reasoning: z - .object({ - effort: z.string().nullable().optional(), - maxTokens: optionalNumber, - mode: z.string().nullable().optional(), - }) - .nullable() - .optional(), - repetitionPenalty: optionalNumber, - responseMimeType: z.string().nullable().optional(), - temperature: optionalNumber, - topK: optionalNumber, - topP: optionalNumber, - }) - .refine((data) => data.minLength == null || data.maxLength == null || data.minLength <= data.maxLength, { - message: 'Min length must not exceed max length', - path: ['minLength'], - }) - .refine((data) => data.reasoning?.maxTokens == null || data.reasoning.maxTokens <= 32000, { - message: 'Maximum 32000 tokens', - path: ['reasoning', 'maxTokens'], - }) - .optional(); - -const formSchema = z.object({ - agents: z.record(z.string(), agentConfigSchema).optional(), - name: requiredString('Provider name is required').pipe(z.string().max(50, 'Maximum 50 characters allowed')), - type: requiredString('Provider type is required'), -}); +// Built as a function of `t` (called from within the component) so validation messages +// follow the active language instead of being frozen in English at module load. +const buildAgentConfigSchema = (t: (key: string) => string) => + z + .object({ + extraBody: optionalJsonObject, + frequencyPenalty: optionalNumber, + json: z.boolean().nullable().optional(), + maxLength: optionalNumber, + maxTokens: optionalNumber, + minLength: optionalNumber, + minP: optionalNumber, + model: requiredString(t('providers.modelRequired')), + n: optionalNumber, + presencePenalty: optionalNumber, + price: z + .object({ + cacheRead: optionalNumber, + cacheWrite: optionalNumber, + input: optionalNumber, + output: optionalNumber, + }) + .nullable() + .optional(), + reasoning: z + .object({ + effort: z.string().nullable().optional(), + maxTokens: optionalNumber, + mode: z.string().nullable().optional(), + }) + .nullable() + .optional(), + repetitionPenalty: optionalNumber, + responseMimeType: z.string().nullable().optional(), + temperature: optionalNumber, + topK: optionalNumber, + topP: optionalNumber, + }) + .refine((data) => data.minLength == null || data.maxLength == null || data.minLength <= data.maxLength, { + message: t('providers.minLengthExceed'), + path: ['minLength'], + }) + .refine((data) => data.reasoning?.maxTokens == null || data.reasoning.maxTokens <= 32000, { + message: t('providers.maxReasoningTokens'), + path: ['reasoning', 'maxTokens'], + }) + .optional(); + +const buildFormSchema = (t: (key: string) => string) => + z.object({ + agents: z.record(z.string(), buildAgentConfigSchema(t)).optional(), + name: requiredString(t('providers.providerNameRequired')).pipe( + z.string().max(50, t('providers.providerNameMax')), + ), + type: requiredString(t('providers.providerTypeRequired')), + }); + +const formSchema = buildFormSchema((key: string) => key); type FormAgents = FormInput['agents']; @@ -719,12 +740,16 @@ const getReasoningMode = (mode: null | string | undefined): null | ReasoningMode } }; -const reasoningEffortLabel: Record = { - [ReasoningEffort.High]: 'High', - [ReasoningEffort.Low]: 'Low', - [ReasoningEffort.Max]: 'Max', - [ReasoningEffort.Medium]: 'Medium', - [ReasoningEffort.Xhigh]: 'Extra High', +const getReasoningEffortLabel = (t: (key: string) => string, effort: ReasoningEffort): string => { + const labels: Record = { + [ReasoningEffort.High]: t('providers.reasoningHigh'), + [ReasoningEffort.Low]: t('providers.reasoningLow'), + [ReasoningEffort.Max]: t('providers.reasoningMax'), + [ReasoningEffort.Medium]: t('providers.reasoningMedium'), + [ReasoningEffort.Xhigh]: t('providers.reasoningExtraHigh'), + }; + + return labels[effort]; }; const defaultReasoningEfforts: ReasoningEffort[] = [ReasoningEffort.Low, ReasoningEffort.Medium, ReasoningEffort.High]; @@ -744,6 +769,7 @@ function ReasoningFields({ models: ModelOption[]; setValue: UseFormSetValue; }) { + const { t } = useI18n(); const selectedModel = useWatch({ control, name: `agents.${agentKey}.model` }); const reasoningMode = useWatch({ control, name: `agents.${agentKey}.reasoning.mode` }); const capability = models.find((model) => model.name === selectedModel)?.reasoning ?? null; @@ -770,7 +796,7 @@ function ReasoningFields({ return (
-

Reasoning Configuration

+

{t('providers.reasoningConfig')}

{(supportsAdaptive || canDisable) && ( ( - Reasoning Mode + {t('providers.reasoningMode')} {isAdaptiveOnly ? canDisable - ? 'This model thinks adaptively; choose Off to disable thinking.' - : 'This model supports only adaptive thinking and cannot be disabled.' - : 'Adaptive lets the model decide how much to think; budget uses a fixed token budget; off disables thinking.'} + ? t('providers.adaptiveThinkingDesc') + : t('providers.adaptiveOnlyDesc') + : t('providers.reasoningModeDesc')} @@ -820,7 +852,7 @@ function ReasoningFields({ name={`agents.${agentKey}.reasoning.effort`} render={({ field }) => ( - Reasoning Effort + {t('providers.reasoningEffort')} @@ -732,12 +583,12 @@ function TemplateForm({ templateId }: { templateId?: string }) { @@ -769,7 +620,7 @@ function TemplateForm({ templateId }: { templateId?: string }) { refetchTemplate()} - title="Error loading template" + title={t('templates.errorLoadingTemplate')} />
@@ -783,9 +634,9 @@ function TemplateForm({ templateId }: { templateId?: string }) {
-

Template not found

-

The template you are looking for does not exist.

- +

{t('templates.notFoundTitle')}

+

{t('templates.notFoundDesc')}

+
@@ -826,9 +677,9 @@ function TemplateForm({ templateId }: { templateId?: string }) { } - confirmText="Replace" + confirmText={t('templates.replace')} confirmVariant="default" - description="Current form has content. Replace with the selected preset?" + description={t('templates.replaceContentDesc')} handleConfirm={handleConfirmReplacePreset} handleOpenChange={(open) => { if (!open) { @@ -838,11 +689,11 @@ function TemplateForm({ templateId }: { templateId?: string }) { setIsReplaceConfirmOpen(open); }} isOpen={isReplaceConfirmOpen} - title="Replace content?" + title={t('templates.replaceContentTitle')} /> { if (!deletingTemplate) { @@ -138,7 +140,7 @@ function Templates() { inputRef={editingInputRef} onCancel={handleTemplateRenameCancel} onSave={handleTemplateRenameSave} - placeholder="Template title" + placeholder={t('templates.titlePlaceholder')} />
); @@ -149,7 +151,7 @@ function Templates() { header: ({ column }) => ( ), meta: { searchable: true }, @@ -164,7 +166,7 @@ function Templates() { header: ({ column }) => ( ), meta: { searchable: true }, @@ -178,7 +180,7 @@ function Templates() { @@ -332,7 +334,7 @@ function Templates() { columns={columns} data={templates} empty={{ entityName: 'templates' }} - filterPlaceholder="Filter templates..." + filterPlaceholder={t('templates.filterTemplates')} filterValue={filter} onFilterChange={setFilter} onRowClick={(template) => { @@ -344,8 +346,8 @@ function Templates() { />