editor.chain().focus().toggleBold().run()}
pressed={state.isBold}
shortcut={shortcutFor('B')}
@@ -278,7 +280,7 @@ export const MarkdownEditorToolbar = memo(function MarkdownEditorToolbar({
editor.chain().focus().toggleItalic().run()}
pressed={state.isItalic}
shortcut={shortcutFor('I')}
@@ -287,7 +289,7 @@ export const MarkdownEditorToolbar = memo(function MarkdownEditorToolbar({
editor.chain().focus().toggleStrike().run()}
pressed={state.isStrike}
>
@@ -295,7 +297,7 @@ export const MarkdownEditorToolbar = memo(function MarkdownEditorToolbar({
editor.chain().focus().toggleCode().run()}
pressed={state.isCode}
>
@@ -344,7 +346,7 @@ export const MarkdownEditorToolbar = memo(function MarkdownEditorToolbar({
>
editor.chain().focus().toggleBlockquote().run()}
pressed={state.isBlockquote}
>
@@ -352,7 +354,7 @@ export const MarkdownEditorToolbar = memo(function MarkdownEditorToolbar({
editor.chain().focus().toggleCodeBlock().run()}
pressed={state.isCodeBlock}
>
@@ -364,7 +366,7 @@ export const MarkdownEditorToolbar = memo(function MarkdownEditorToolbar({
/>
editor.chain().focus().setHorizontalRule().run()}
>
@@ -378,7 +380,7 @@ export const MarkdownEditorToolbar = memo(function MarkdownEditorToolbar({
editor.chain().focus().unsetAllMarks().clearNodes().run()}
>
@@ -396,7 +398,7 @@ export const MarkdownEditorToolbar = memo(function MarkdownEditorToolbar({
>
editor.chain().focus().undo().run()}
shortcut={shortcutFor('Z')}
>
@@ -404,7 +406,7 @@ export const MarkdownEditorToolbar = memo(function MarkdownEditorToolbar({
editor.chain().focus().redo().run()}
shortcut={shiftShortcutFor('Z')}
>
diff --git a/frontend/src/components/shared/markdown-editor/markdown-editor-view-mode.tsx b/frontend/src/components/shared/markdown-editor/markdown-editor-view-mode.tsx
index c98669297..0511bd988 100644
--- a/frontend/src/components/shared/markdown-editor/markdown-editor-view-mode.tsx
+++ b/frontend/src/components/shared/markdown-editor/markdown-editor-view-mode.tsx
@@ -1,6 +1,7 @@
import { SquareMenu, Type } from 'lucide-react';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { useI18n } from '@/hooks/use-i18n';
// 'rich' reflows whitespace on save (tiptap); 'raw' is a byte-exact textarea over the source.
export type EditorViewMode = 'raw' | 'rich';
@@ -13,6 +14,8 @@ interface EditorViewModeToggleProps {
}
export function EditorViewModeToggle({ className, mode, onModeChange, rawTooltip }: EditorViewModeToggleProps) {
+ const { t } = useI18n();
+
return (
(value);
@@ -189,7 +192,7 @@ function useMarkdownEditor({
// plugin regardless; keeping it out of the deps makes that explicit and stops rebuilding the whole
// extension array on every placeholder change.
// eslint-disable-next-line react-hooks/exhaustive-deps
- const extensions = useMemo(() => createMarkdownExtensions(placeholder), []);
+ const extensions = useMemo(() => createMarkdownExtensions(resolvedPlaceholder), []);
const editor = useEditor({
content: initialContent,
diff --git a/frontend/src/components/shared/overwrite/overwrite-dialog.tsx b/frontend/src/components/shared/overwrite/overwrite-dialog.tsx
index ab91f4e65..18a246532 100644
--- a/frontend/src/components/shared/overwrite/overwrite-dialog.tsx
+++ b/frontend/src/components/shared/overwrite/overwrite-dialog.tsx
@@ -1,6 +1,8 @@
import { Replace } from 'lucide-react';
import ConfirmationDialog from '@/components/shared/confirmation-dialog';
+import { useI18n } from '@/hooks/use-i18n';
+import i18n from '@/i18n/config';
export interface OverwriteConflict {
destination: string;
@@ -36,17 +38,21 @@ const buildDefaultDescription = (conflicts: OverwriteConflict[]): string | undef
const single = conflicts.length === 1 ? conflicts[0] : undefined;
if (single) {
- return `An item named "${single.destinationName}" already exists at /${single.destination}. Do you want to replace it?`;
+ return i18n.t('overwrite.singleConflictDescription', {
+ destination: single.destination,
+ name: single.destinationName,
+ });
}
if (conflicts.length > 1) {
- return `${conflicts.length} items already exist at the destination. Do you want to replace all of them?`;
+ return i18n.t('overwrite.batchConflictDescription', { count: conflicts.length });
}
return undefined;
};
-const buildDefaultConfirmText = (count: number): string => (count > 1 ? 'Replace all' : 'Replace');
+const buildDefaultConfirmText = (count: number): string =>
+ count > 1 ? i18n.t('overwrite.replaceAll') : i18n.t('overwrite.replace');
/**
* Shared "Replace or cancel" confirmation for destructive overwrite flows
@@ -63,11 +69,13 @@ export function OverwriteDialog({
description,
onCancel,
onReplaceAll,
- title = 'Replace existing item?',
+ title,
}: OverwriteDialogProps) {
+ const { t } = useI18n();
+
return (
}
confirmText={confirmText ?? buildDefaultConfirmText(conflicts.length)}
confirmVariant="destructive"
@@ -81,7 +89,7 @@ export function OverwriteDialog({
}
}}
isOpen={conflicts.length > 0}
- title={title}
+ title={title ?? t('overwrite.replaceTitle')}
/>
);
}
diff --git a/frontend/src/components/shared/route-error-boundary.tsx b/frontend/src/components/shared/route-error-boundary.tsx
index 75f3d50c9..555ba723b 100644
--- a/frontend/src/components/shared/route-error-boundary.tsx
+++ b/frontend/src/components/shared/route-error-boundary.tsx
@@ -4,6 +4,7 @@ import { useRouteError } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty';
+import { useI18n } from '@/hooks/use-i18n';
import { isChunkLoadError, isDomDesyncError, reloadOnce } from '@/lib/chunk-reload';
/**
@@ -14,6 +15,7 @@ import { isChunkLoadError, isDomDesyncError, reloadOnce } from '@/lib/chunk-relo
* desync from browser auto-translation) — both shown as a recoverable card.
*/
function RouteErrorBoundary() {
+ const { t } = useI18n();
const error = useRouteError();
const isChunk = isChunkLoadError(error);
const isDesync = isDomDesyncError(error);
@@ -34,13 +36,13 @@ function RouteErrorBoundary() {
- Something went wrong
+ {t('errors.somethingWentWrong')}
{isChunk
- ? 'A new version was likely just deployed. Reloading will load the latest one.'
+ ? t('errors.chunkLoadError')
: isDesync
- ? 'The page hit a display glitch. Reloading usually clears it.'
- : 'The page ran into an unexpected error. Reloading usually clears it.'}
+ ? t('errors.domDesyncError')
+ : t('errors.unexpectedError')}
@@ -48,7 +50,7 @@ function RouteErrorBoundary() {
onClick={() => window.location.reload()}
variant="secondary"
>
- Reload
+ {t('common.reload')}
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 (
- {totalRows > 0 ? (
- <>
- Showing {rangeStart}–{rangeEnd} of {totalRows}
- >
- ) : empty?.entityName ? (
- `No ${empty.entityName}`
- ) : (
- 'No results'
- )}
+ {totalRows > 0
+ ? t('common.showingRange', { end: rangeEnd, start: rangeStart, total: totalRows })
+ : empty?.entityName
+ ? t('common.noEntity', { entity: empty.entityName })
+ : t('common.noResultsPlain')}
- Rows per page
+ {t('common.rowsPerPage')}
{pageCount > 0 ? (
- Page {safePageIndex + 1} of {pageCount}
+ {t('common.pageOf', { page: safePageIndex + 1, pageCount })}
) : (
({
)}
table.firstPage()}
size="icon-xs"
@@ -906,7 +910,7 @@ function DataTable({
table.previousPage()}
size="icon-xs"
@@ -915,7 +919,7 @@ function DataTable({
table.nextPage()}
size="icon-xs"
@@ -924,7 +928,7 @@ function DataTable({
table.lastPage()}
size="icon-xs"
@@ -997,6 +1001,7 @@ function DataTableColumnHeader({ column, title }: DataT
* was the entire class of races the previous design carried.
*/
function DataTableFilter({ onQueryChange, placeholder, query }: DataTableFilterProps) {
+ const { t } = useI18n();
const [localValue, setLocalValue] = useState(query);
const lastEmittedReference = useRef(query);
// Generated per-instance so pages with multiple DataTables (e.g.
@@ -1062,7 +1067,7 @@ function DataTableFilter({ onQueryChange, placeholder, query }: DataTableFilterP
{localValue ? (
diff --git a/frontend/src/components/ui/dialog.tsx b/frontend/src/components/ui/dialog.tsx
index 6efcbab62..c6e8e41d5 100644
--- a/frontend/src/components/ui/dialog.tsx
+++ b/frontend/src/components/ui/dialog.tsx
@@ -2,6 +2,7 @@ import * as DialogPrimitive from '@radix-ui/react-dialog';
import { X } from 'lucide-react';
import * as React from 'react';
+import { useI18n } from '@/hooks/use-i18n';
import { cn } from '@/lib/utils';
function Dialog({ ...props }: React.ComponentProps) {
@@ -23,6 +24,8 @@ function DialogClose({ ...props }: React.ComponentProps) {
+ const { t } = useI18n();
+
return (
@@ -45,7 +48,7 @@ function DialogContent({ children, className, ...props }: React.ComponentProps
{/* not "Close": pages render visible Close buttons, and duplicate accessible names break role-based locators */}
- Dismiss dialog
+ {t('common.dismissDialog')}
diff --git a/frontend/src/components/ui/sheet.tsx b/frontend/src/components/ui/sheet.tsx
index 3735bec47..781eb7196 100644
--- a/frontend/src/components/ui/sheet.tsx
+++ b/frontend/src/components/ui/sheet.tsx
@@ -6,6 +6,7 @@ import { X } from 'lucide-react';
import * as React from 'react';
import { FocusReturn } from '@/components/ui/dialog';
+import { useI18n } from '@/hooks/use-i18n';
import { cn } from '@/lib/utils';
function Sheet({ ...props }: React.ComponentProps) {
@@ -81,6 +82,8 @@ interface SheetContentProps
}
function SheetContent({ children, className, container, overlay = true, side = 'right', ...props }: SheetContentProps) {
+ const { t } = useI18n();
+
return (
{overlay && }
@@ -94,7 +97,7 @@ function SheetContent({ children, className, container, overlay = true, side = '
{/* Not "Close": a sheet with its own footer Close button would
produce two identically named controls. */}
- Dismiss sheet
+ {t('common.dismissSheet')}
{children}
diff --git a/frontend/src/components/ui/sidebar.tsx b/frontend/src/components/ui/sidebar.tsx
index 69db9462e..87d25d0ed 100644
--- a/frontend/src/components/ui/sidebar.tsx
+++ b/frontend/src/components/ui/sidebar.tsx
@@ -12,6 +12,7 @@ import { Sheet, SheetContent, SheetDescription, SheetTitle } from '@/components/
import { Skeleton } from '@/components/ui/skeleton';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { useBreakpoint } from '@/hooks/use-breakpoint';
+import { useI18n } from '@/hooks/use-i18n';
import { cn } from '@/lib/utils';
const SIDEBAR_COOKIE_NAME = 'sidebar:state';
@@ -73,6 +74,7 @@ function Sidebar({
variant?: 'floating' | 'inset' | 'sidebar';
}) {
const { isMobile, openMobile, setOpenMobile, state } = useSidebar();
+ const { t } = useI18n();
if (collapsible === 'none') {
return (
@@ -109,8 +111,8 @@ function Sidebar({
} as React.CSSProperties
}
>
- Sidebar
- Displays the mobile sidebar.
+ {t('common.sidebar')}
+ {t('common.displaysMobileSidebar')}
{children}
@@ -414,10 +416,11 @@ function SidebarProvider({
function SidebarRail({ className, ...props }: React.ComponentProps<'button'>) {
const { toggleSidebar } = useSidebar();
+ const { t } = useI18n();
return (
) {
data-sidebar="rail"
onClick={toggleSidebar}
tabIndex={-1}
- title="Toggle Sidebar"
+ title={t('common.toggleSidebar')}
{...props}
/>
);
@@ -448,6 +451,7 @@ function SidebarSeparator({ className, ...props }: React.ComponentProps) {
const { toggleSidebar } = useSidebar();
+ const { t } = useI18n();
return (
- Toggle Sidebar
+ {t('common.toggleSidebar')}
);
}
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')}
window.close()}
>
- Close
+ {t('common.close')}
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 && (
@@ -312,11 +314,11 @@ function Flow() {
onSelect={(event) => event.preventDefault()}
>
- Flows
+ {t('common.flows')}
controller={flowNav}
- sheetTitle="Flows"
+ sheetTitle={t('common.flows')}
size="sm"
/>
@@ -333,7 +335,9 @@ function Flow() {
: 'size-4'
}
/>
- {isFavoriteFlow(flowId) ? 'Remove from favorites' : 'Add to favorites'}
+ {isFavoriteFlow(flowId)
+ ? t('flow.removeFromFavorites')
+ : t('flow.addToFavorites')}
)}
@@ -344,7 +348,7 @@ function Flow() {
onClick={handleFlowRenameStart}
>
- Rename
+ {t('common.rename')}
{isFlowRunning && (
- Finishing...
+ {t('flow.finishing')}
>
) : (
<>
- Finish
+ {t('common.finish')}
>
)}
@@ -372,12 +376,12 @@ function Flow() {
{isDeleting ? (
<>
- Deleting...
+ {t('flow.deleting')}
>
) : (
<>
- Delete
+ {t('common.delete')}
>
)}
@@ -388,9 +392,9 @@ function Flow() {
{isMobile && (
controller={flowNav}
- renderItem={renderFlowItem}
+ renderItem={(item, isCurrent) => renderFlowItem(item, isCurrent, t)}
sheetIcon={}
- sheetTitle="Flows"
+ sheetTitle={t('common.flows')}
/>
)}
@@ -432,19 +436,20 @@ function Flow() {
)}
>
);
}
function FlowReportDropdown() {
+ const { t } = useI18n();
const { flowData, flowId } = useFlow();
const flow = flowData?.flow;
const tasks = flowData?.tasks ?? [];
@@ -460,10 +465,10 @@ function FlowReportDropdown() {
const success = await copyToClipboard(reportContent);
if (success) {
- toast.success('Report copied to clipboard');
+ toast.success(t('flow.reportCopied'));
} else {
Log.error('Failed to copy report to clipboard');
- toast.error('Failed to copy report to clipboard');
+ toast.error(t('flow.reportCopyFailed'));
}
};
@@ -510,7 +515,7 @@ function FlowReportDropdown() {
disabled={isReportDisabled}
endIcon={}
icon={}
- label="Report"
+ label={t('flow.report')}
variant="ghost"
/>
@@ -521,7 +526,7 @@ function FlowReportDropdown() {
onClick={handleOpenWebView}
>
- Open web view
+ {t('flow.openWebView')}
- Copy to clipboard
+ {t('flow.copyToClipboard')}
- Download MD
+ {t('flow.downloadMD')}
- Download PDF
+ {t('flow.downloadPDF')}
diff --git a/frontend/src/pages/flows/flows.tsx b/frontend/src/pages/flows/flows.tsx
index 3dbf69df1..0472f580d 100644
--- a/frontend/src/pages/flows/flows.tsx
+++ b/frontend/src/pages/flows/flows.tsx
@@ -36,6 +36,7 @@ import { Spinner } from '@/components/ui/spinner';
import { Toggle } from '@/components/ui/toggle';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { RenameFlowDocument, ResultType, StatusType, type TerminalFragmentFragment } from '@/graphql/types';
+import { useI18n } from '@/hooks/use-i18n';
import { useTableState } from '@/hooks/use-table-state';
import { routes } from '@/lib/routes';
import { mergeHrefWithSearchParams } from '@/lib/url-params';
@@ -43,35 +44,36 @@ import { formatDate } from '@/lib/utils/format';
import { useFavorites } from '@/providers/favorites-provider';
import { type Flow, useFlows } from '@/providers/flows-provider';
-const statusConfig: Record<
- StatusType,
- { label: string; variant: 'default' | 'destructive' | 'outline' | 'secondary' }
-> = {
- [StatusType.Created]: {
- label: 'Created',
- variant: 'outline',
- },
- [StatusType.Failed]: {
- label: 'Failed',
- variant: 'destructive',
- },
- [StatusType.Finished]: {
- label: 'Finished',
- variant: 'secondary',
- },
- [StatusType.Running]: {
- label: 'Running',
- variant: 'default',
- },
- [StatusType.Waiting]: {
- label: 'Waiting',
- variant: 'outline',
- },
-};
-
function Flows() {
const navigate = useNavigate();
const location = useLocation();
+ const { t } = useI18n();
+
+ const statusConfig: Record<
+ StatusType,
+ { label: string; variant: 'default' | 'destructive' | 'outline' | 'secondary' }
+ > = {
+ [StatusType.Created]: {
+ label: t('flow.created'),
+ variant: 'outline',
+ },
+ [StatusType.Failed]: {
+ label: t('flow.failed'),
+ variant: 'destructive',
+ },
+ [StatusType.Finished]: {
+ label: t('flow.finished'),
+ variant: 'secondary',
+ },
+ [StatusType.Running]: {
+ label: t('flow.running'),
+ variant: 'default',
+ },
+ [StatusType.Waiting]: {
+ label: t('flow.waiting'),
+ variant: 'outline',
+ },
+ };
const { deleteFlow, finishFlow, flows, flowsError, isLoading, refetch } = useFlows();
const { isFavoriteFlow, toggleFavoriteFlow } = useFavorites();
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
@@ -139,14 +141,14 @@ function Flows() {
});
if (data?.renameFlow === ResultType.Success) {
- toast.success('Flow renamed successfully');
+ toast.success(t('flow.renameSuccess'));
setEditingFlowId(null);
}
} 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);
}
- }, [editingFlowId, renameFlowMutation]);
+ }, [editingFlowId, renameFlowMutation, t]);
const handleFlowRenameCancel = useCallback(() => {
setEditingFlowId(null);
@@ -179,7 +181,7 @@ function Flows() {
header: ({ column }) => (
),
maxSize: 80,
@@ -204,7 +206,7 @@ function Flows() {
inputRef={editingInputRef}
onCancel={handleFlowRenameCancel}
onSave={handleFlowRenameSave}
- placeholder="Flow title"
+ placeholder={t('flow.flowTitle')}
/>
);
@@ -216,7 +218,7 @@ function Flows() {
header: ({ column }) => (
),
meta: { searchable: true },
@@ -242,7 +244,7 @@ function Flows() {
header: ({ column }) => (
),
maxSize: 130,
@@ -266,14 +268,14 @@ function Flows() {
className="size-4"
provider={flow.provider}
/>
- {flow.provider?.name || 'N/A'}
+ {flow.provider?.name || t('common.notAvailable')}
);
},
header: ({ column }) => (
),
id: 'provider',
@@ -299,7 +301,7 @@ function Flows() {
const terminals = flow.terminals || [];
if (terminals.length === 0) {
- return No terminals;
+ return {t('flow.noTerminals')};
}
const isAnyConnected = terminals.some((t: TerminalFragmentFragment) => t.connected);
@@ -326,7 +328,7 @@ function Flows() {
>
{terminal.image}
- ({terminal.connected ? 'connected' : 'disconnected'})
+ ({terminal.connected ? t('flow.connected') : t('flow.disconnected')})
))}
@@ -338,7 +340,7 @@ function Flows() {
header: ({ column }) => (
),
id: 'terminals',
@@ -363,11 +365,11 @@ function Flows() {
header: ({ column }) => (
),
maxSize: 140,
- meta: { columnMenuLabel: 'Created' },
+ meta: { columnMenuLabel: t('flow.created') },
minSize: 100,
size: 120,
sortingFn: (rowA, rowB) => {
@@ -387,11 +389,11 @@ function Flows() {
header: ({ column }) => (
),
maxSize: 140,
- meta: { columnMenuLabel: 'Updated' },
+ meta: { columnMenuLabel: t('flow.updatedAt') },
minSize: 100,
size: 120,
sortingFn: (rowA, rowB) => {
@@ -409,7 +411,7 @@ function Flows() {
return (
{
event.stopPropagation();
@@ -424,7 +426,7 @@ function Flows() {
e.stopPropagation()}
variant="ghost"
@@ -439,11 +441,11 @@ function Flows() {
>
handleFlowOpen(flow.id)}>
- View
+ {t('common.view')}
handleFlowRenameStart(flow)}>
- Rename
+ {t('common.rename')}
{isRunning && (
- Finishing...
+ {t('flow.finishing')}
>
) : (
<>
- Finish
+ {t('common.finish')}
>
)}
@@ -471,12 +473,12 @@ function Flows() {
{deletingFlowIds.has(flow.id) ? (
<>
- Deleting...
+ {t('flow.deleting')}
>
) : (
<>
- Delete
+ {t('common.delete')}
>
)}
@@ -506,6 +508,7 @@ function Flows() {
handleFlowRenameStart,
isFavoriteFlow,
isRenameLoading,
+ t,
toggleFavoriteFlow,
],
);
@@ -518,16 +521,16 @@ function Flows() {
<>
toggleFavoriteFlow(flow.id)}>
- {isFavoriteFlow(flow.id) ? 'Remove from favorites' : 'Add to favorites'}
+ {isFavoriteFlow(flow.id) ? t('flow.removeFromFavorites') : t('flow.addToFavorites')}
handleFlowOpen(flow.id)}>
- View
+ {t('common.view')}
handleFlowRenameStart(flow)}>
- Rename
+ {t('common.rename')}
{isRunning && (
@@ -536,7 +539,7 @@ function Flows() {
onClick={() => handleFlowFinish(flow)}
>
- {finishingFlowIds.has(flow.id) ? 'Finishing...' : 'Finish'}
+ {finishingFlowIds.has(flow.id) ? t('flow.finishing') : t('common.finish')}
)}
@@ -545,7 +548,7 @@ function Flows() {
onClick={() => handleFlowDeleteDialogOpen(flow)}
>
- {deletingFlowIds.has(flow.id) ? 'Deleting...' : 'Delete'}
+ {deletingFlowIds.has(flow.id) ? t('flow.deleting') : t('common.delete')}
>
);
@@ -558,6 +561,7 @@ function Flows() {
handleFlowOpen,
handleFlowRenameStart,
isFavoriteFlow,
+ t,
toggleFavoriteFlow,
],
);
@@ -574,12 +578,12 @@ function Flows() {
const pageHeader = (
- }>Flows
+ }>{t('common.flows')}
}
- label="New Flow"
+ label={t('common.newFlow')}
onClick={() => navigate(routes.newFlow)}
variant="secondary"
/>
@@ -593,8 +597,8 @@ function Flows() {
{pageHeader}
>
@@ -610,7 +614,7 @@ function Flows() {
>
@@ -627,8 +631,8 @@ function Flows() {
- No flows found
- Get started by creating your first conversation flow
+ {t('flow.noFlowsFound')}
+ {t('flow.getStarted')}
- New Flow
+ {t('common.newFlow')}
@@ -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() {
event.stopPropagation()}
variant="ghost"
@@ -318,11 +320,11 @@ function Knowledges() {
>
handleOpen(k.id)}>
- Edit
+ {t('common.edit')}
handleKnowledgeRenameStart(k)}>
- Rename
+ {t('common.rename')}
- Deleting...
+ {t('knowledges.deleting')}
>
) : (
<>
- Delete
+ {t('common.delete')}
>
)}
@@ -360,11 +362,11 @@ function Knowledges() {
<>
handleOpen(k.id)}>
- Edit
+ {t('common.edit')}
handleKnowledgeRenameStart(k)}>
- Rename
+ {t('common.rename')}
handleDeleteDialogOpen(k)}
>
- {deletingIds.has(k.id) ? 'Deleting...' : 'Delete'}
+ {deletingIds.has(k.id) ? t('knowledges.deleting') : t('common.delete')}
>
);
@@ -380,21 +382,23 @@ function Knowledges() {
const pageHeader = (
- }>Knowledges
+ }>
+ {t('knowledges.title')}
+
}
- label="New Knowledge"
+ label={t('knowledges.newKnowledge')}
onClick={() => navigate(routes.newKnowledge)}
variant="secondary"
/>
@@ -408,8 +412,8 @@ function Knowledges() {
{pageHeader}
>
@@ -425,7 +429,7 @@ function Knowledges() {
>
@@ -442,10 +446,8 @@ function Knowledges() {
- No knowledge documents yet
-
- Create your first knowledge document to enrich the vector store
-
+ {t('knowledges.noKnowledgesTitle')}
+ {t('knowledges.noKnowledgesDesc')}
- New Knowledge
+ {t('knowledges.newKnowledge')}
@@ -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() {
toggleViewOption('size')}
onSelect={(event) => event.preventDefault()}
>
- Size
+ {t('resources.sizeColumn')}
toggleViewOption('modified')}
onSelect={(event) => event.preventDefault()}
>
- Modified
+ {t('resources.modifiedColumn')}
toggleViewOption('foldersFirst')}
onSelect={(event) => event.preventDefault()}
>
- Folders first
+ {t('resources.foldersFirst')}
toggleViewOption('isModifiedRelative')}
onSelect={(event) => event.preventDefault()}
>
- Relative dates
+ {t('resources.relativeDates')}
@@ -587,13 +597,13 @@ 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') && (
- Change
+ {t('account.change')}
)}
@@ -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') && (
- Change
+ {t('account.change')}
)}
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 (
:
}
void;
}) {
const { isValid } = useFormState({ control });
+ const { t } = useTranslation();
return (
:
}
{
@@ -372,11 +378,11 @@ function SettingsAPITokens() {
setCreatingToken(false);
createForm.reset(CREATE_TOKEN_DEFAULTS);
} catch (error) {
- toast.error('Failed to create token', {
+ toast.error(t('apiTokens.createTokenFailed'), {
description: error instanceof Error ? error.message : undefined,
});
}
- }, [createAPIToken, createForm]);
+ }, [createAPIToken, createForm, t]);
const handleDeleteDialogOpen = useCallback((token: APIToken) => {
setDeletingToken(token);
@@ -397,25 +403,28 @@ function SettingsAPITokens() {
setDeletingToken(null);
} catch (error) {
- toast.error('Failed to delete token', {
+ toast.error(t('apiTokens.deleteTokenFailed'), {
description: error instanceof Error ? error.message : undefined,
});
}
},
- [deleteAPIToken],
+ [deleteAPIToken, t],
);
- const handleCopyTokenId = useCallback(async (tokenId: string) => {
- const success = await copyToClipboard(tokenId);
+ const handleCopyTokenId = useCallback(
+ async (tokenId: string) => {
+ const success = await copyToClipboard(tokenId);
- if (success) {
- toast.success('Token ID copied to clipboard');
+ if (success) {
+ toast.success(t('apiTokens.tokenIdCopied'));
- return;
- }
+ return;
+ }
- toast.error('Failed to copy token ID to clipboard');
- }, []);
+ toast.error(t('apiTokens.tokenIdCopyFailed'));
+ },
+ [t],
+ );
const columns: ColumnDef[] = useMemo(
() => [
@@ -438,7 +447,7 @@ function SettingsAPITokens() {
autoFocus
className="h-8"
id={createNameFieldId}
- placeholder="Token name (optional)"
+ placeholder={t('apiTokens.tokenNamePlaceholder')}
/>
)}
/>
@@ -457,7 +466,7 @@ function SettingsAPITokens() {
autoFocus
className="h-8"
id={editNameFieldId}
- placeholder="Token name (optional)"
+ placeholder={t('apiTokens.tokenNamePlaceholder')}
/>
)}
/>
@@ -466,7 +475,9 @@ function SettingsAPITokens() {
return (
- {token.name || (unnamed)}
+ {token.name || (
+ {t('apiTokens.unnamed')}
+ )}
);
},
@@ -474,7 +485,7 @@ function SettingsAPITokens() {
header: ({ column }) => (
),
meta: { searchable: true },
@@ -487,7 +498,7 @@ function SettingsAPITokens() {
const isCreating = token.id === 'create-new';
if (isCreating) {
- return N/A
;
+ return {t('common.notAvailable')}
;
}
const tokenId = row.getValue('tokenId') as string;
@@ -496,7 +507,7 @@ function SettingsAPITokens() {
{tokenId}
handleCopyTokenId(tokenId)}
variant="ghost"
@@ -510,10 +521,10 @@ function SettingsAPITokens() {
header: ({ column }) => (
),
- meta: { columnMenuLabel: 'Token ID', searchable: true },
+ meta: { columnMenuLabel: t('apiTokens.tokenId'), searchable: true },
size: 200,
},
{
@@ -523,12 +534,12 @@ function SettingsAPITokens() {
const isCreating = token.id === 'create-new';
if (isCreating) {
- return active;
+ return {t('apiTokens.active')};
}
const isEditing = editingTokenId === token.tokenId;
const expired = isTokenExpired(token);
- const statusDisplay = getStatusDisplay(token);
+ const statusDisplay = getStatusDisplay(token, t);
if (isEditing) {
if (expired) {
@@ -549,8 +560,8 @@ function SettingsAPITokens() {
- active
- revoked
+ {t('apiTokens.active')}
+ {t('apiTokens.revoked')}
@@ -564,7 +575,7 @@ function SettingsAPITokens() {
header: ({ column }) => (
),
meta: { searchable: true },
@@ -598,9 +609,9 @@ function SettingsAPITokens() {
>
{field.value ? (
- format(field.value, 'd MMM yyyy', { locale: enUS })
+ format(field.value, 'd MMM yyyy', { locale: dateLocale })
) : (
- Pick date
+ {t('apiTokens.pickDate')}
)}
@@ -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() {
handleEdit(token)}>
- Edit
+ {t('common.edit')}
handleCopyTokenId(token.tokenId)}>
- Copy Token ID
+ {t('apiTokens.copyTokenId')}
- Deleting...
+ {t('apiTokens.deleting')}
>
) : (
<>
- Delete
+ {t('common.delete')}
>
)}
@@ -781,11 +792,11 @@ function SettingsAPITokens() {
<>
handleEdit(token)}>
- Edit
+ {t('common.edit')}
handleCopyTokenId(token.tokenId)}>
- Copy Token ID
+ {t('apiTokens.copyTokenId')}
handleDeleteDialogOpen(token)}
>
- {isDeleteLoading && deletingToken?.tokenId === token.tokenId ? 'Deleting...' : 'Delete'}
+ {isDeleteLoading && deletingToken?.tokenId === token.tokenId
+ ? t('apiTokens.deleting')
+ : t('common.delete')}
>
);
},
- [deletingToken, handleCopyTokenId, handleDeleteDialogOpen, handleEdit, isDeleteLoading],
+ [deletingToken, handleCopyTokenId, handleDeleteDialogOpen, handleEdit, isDeleteLoading, t],
);
const pageHeader = (
- }>API Tokens
+ }>{t('apiTokens.title')}
}
- label="Create Token"
+ label={t('apiTokens.createToken')}
onClick={handleCreateNew}
variant="secondary"
/>
- GraphQL Playground
+ {t('apiTokens.graphqlPlayground')}
@@ -845,7 +858,7 @@ function SettingsAPITokens() {
target="_blank"
>
- Swagger UI
+ {t('apiTokens.swaggerUi')}
@@ -860,8 +873,8 @@ function SettingsAPITokens() {
{pageHeader}
>
@@ -877,7 +890,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')}
- Create Token
+ {t('apiTokens.createToken')}
@@ -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')}
- Close
+ {t('common.close')}
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"
/>
setIsDiffDialogOpen(true)}>
- Diff
+ {t('common.diff')}
{isDeleteLoading ? : }
- {isDeleteLoading ? 'Resetting...' : 'Reset'}
+ {isDeleteLoading ? t('prompts.resetting') : t('common.reset')}
>
@@ -725,12 +741,12 @@ function SettingsPromptEditor({ promptId }: { promptId?: string }) {
className="cursor-default gap-4 hover:bg-transparent focus:bg-transparent"
onSelect={(event) => event.preventDefault()}
>
- View
+ {t('common.view')}
@@ -746,8 +762,8 @@ function SettingsPromptEditor({ promptId }: { promptId?: string }) {
{pageHeader}
>
@@ -762,7 +778,7 @@ function SettingsPromptEditor({ promptId }: { promptId?: string }) {
>
@@ -779,8 +795,8 @@ function SettingsPromptEditor({ promptId }: { promptId?: string }) {
- Prompt not found
- {`The prompt "${promptId}" could not be found or is not supported for editing.`}
+ {t('prompts.promptNotFound')}
+ {t('prompts.promptNotFoundDesc', { id: promptId })}
@@ -794,11 +810,11 @@ function SettingsPromptEditor({ promptId }: { promptId?: string }) {
const promptPanel = (
<>
-
Edit prompt
+
{t('prompts.editPromptTitle')}
{promptInfo.type === 'agent'
- ? 'Customize the templates this agent uses'
- : 'Customize the template this tool uses'}
+ ? t('prompts.customizeAgentTemplates')
+ : t('prompts.customizeToolTemplate')}
@@ -813,8 +829,8 @@ function SettingsPromptEditor({ promptId }: { promptId?: string }) {
{promptInfo.type === 'agent'
- ? 'Configure prompts for this AI agent'
- : 'Configure the prompt for this tool'}
+ ? t('prompts.configureAgentPrompts')
+ : t('prompts.configureToolPrompt')}
@@ -824,7 +840,7 @@ function SettingsPromptEditor({ promptId }: { promptId?: string }) {
value="system"
>
- System Prompt
+ {t('prompts.systemPromptLabel')}
- Human Prompt
+ {t('prompts.humanPromptLabel')}
@@ -846,8 +862,7 @@ function SettingsPromptEditor({ promptId }: { promptId?: string }) {
>
);
- const systemPlaceholder =
- promptInfo.type === 'tool' ? 'Enter the tool template...' : 'Enter the system prompt template...';
+ const systemPlaceholder = promptInfo.type === 'tool' ? t('prompts.toolPlaceholder') : t('prompts.systemPlaceholder');
const promptEditor = (
<>
@@ -863,7 +878,7 @@ function SettingsPromptEditor({ promptId }: { promptId?: string }) {
onSubmit={systemForm.handleSubmit(handleSystemSubmit)}
>
@@ -925,18 +940,18 @@ function SettingsPromptEditor({ promptId }: { promptId?: string }) {
}
- confirmText="Reset"
+ confirmText={t('common.reset')}
confirmVariant="destructive"
- description="Are you sure you want to reset this prompt to its default value? This action cannot be undone."
+ description={t('prompts.resetPromptDesc')}
handleConfirm={handleConfirmReset}
handleOpenChange={setResetDialogOpen}
isOpen={resetDialogOpen}
itemName={`${activeTab} prompt`}
itemType="template"
- title="Reset Prompt"
+ title={t('prompts.resetPromptConfirm')}
/>
- Validation Results
+ {t('prompts.validationResults')}
- The validation result for the {activeTab} prompt template.
+ {t('prompts.validationResultDesc', { tab: activeTab })}
@@ -973,19 +988,21 @@ function SettingsPromptEditor({ promptId }: { promptId?: string }) {
)}
- {validationResult.result === 'success' ? 'Valid Template' : 'Validation Error'}
+ {validationResult.result === 'success'
+ ? t('prompts.validTemplate')
+ : t('prompts.validationError')}
{validationResult.message}
{validationResult.details && (
- Details: {validationResult.details}
+ {t('prompts.details')}: {validationResult.details}
)}
{validationResult.line && (
- Line: {validationResult.line}
+ {t('prompts.lineLabel')}: {validationResult.line}
)}
@@ -993,7 +1010,7 @@ function SettingsPromptEditor({ promptId }: { promptId?: string }) {
- setValidationDialogOpen(false)}>Close
+ setValidationDialogOpen(false)}>{t('common.close')}
)}
@@ -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() {
@@ -385,7 +390,7 @@ function SettingsPrompts() {
>
handlePromptEdit(agent.name)}>
- Edit
+ {t('common.edit')}
{(canResetPrompt(agent.name, 'system') ||
canResetPrompt(agent.name, 'human') ||
@@ -407,12 +412,12 @@ function SettingsPrompts() {
className="size-3"
variant="circle"
/>
- Resetting...
+ {t('prompts.resetting')}
>
) : (
<>
- Reset System
+ {t('prompts.resetSystem')}
>
)}
@@ -434,12 +439,12 @@ function SettingsPrompts() {
className="size-3"
variant="circle"
/>
- Resetting...
+ {t('prompts.resetting')}
>
) : (
<>
- Reset Human
+ {t('prompts.resetHuman')}
>
)}
@@ -461,12 +466,12 @@ function SettingsPrompts() {
className="size-3"
variant="circle"
/>
- Resetting...
+ {t('prompts.resetting')}
>
) : (
<>
- Reset All
+ {t('prompts.resetAll')}
>
)}
@@ -502,26 +507,26 @@ function SettingsPrompts() {
onClick={() => handleColumnSort(column)}
variant="link"
>
- Tool Name
+ {t('prompts.toolName')}
{sorted === 'asc' ? : sorted === 'desc' ? : null}
);
},
- 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() {
@@ -546,7 +551,7 @@ function SettingsPrompts() {
>
handlePromptEdit(tool.name)}>
- Edit
+ {t('common.edit')}
{canResetPrompt(tool.name, 'tool') && (
<>
@@ -567,12 +572,12 @@ function SettingsPrompts() {
className="size-3"
variant="circle"
/>
- Resetting...
+ {t('prompts.resetting')}
>
) : (
<>
- Reset
+ {t('common.reset')}
>
)}
@@ -602,7 +607,7 @@ function SettingsPrompts() {
return (
-
Prompt Templates
+
{t('prompts.promptTemplates')}
@@ -610,13 +615,13 @@ function SettingsPrompts() {
- System Prompt
+ {t('prompts.systemPrompt')}
{userSystemPrompt && (
- Custom
+ {t('common.custom')}
)}
@@ -630,13 +635,13 @@ function SettingsPrompts() {
- Human Prompt
+ {t('prompts.humanPrompt')}
{userHumanPrompt && (
- Custom
+ {t('common.custom')}
)}
@@ -660,13 +665,13 @@ function SettingsPrompts() {
return (
-
Template
+ {t('prompts.template')}
{userToolPrompt && (
- Custom
+ {t('common.custom')}
)}
@@ -687,7 +692,7 @@ function SettingsPrompts() {
<>
handlePromptEdit(agent.name)}>
- Edit
+ {t('common.edit')}
{hasResetOptions &&
}
{canResetPrompt(agent.name, 'system') && (
@@ -703,8 +708,8 @@ function SettingsPrompts() {
{isDeleteLoading &&
resetOperation?.promptName === agent.name &&
resetOperation?.type === 'system'
- ? 'Resetting...'
- : 'Reset System'}
+ ? t('prompts.resetting')
+ : t('prompts.resetSystem')}
)}
{agent.hasHuman && canResetPrompt(agent.name, 'human') && (
@@ -720,8 +725,8 @@ function SettingsPrompts() {
{isDeleteLoading &&
resetOperation?.promptName === agent.name &&
resetOperation?.type === 'human'
- ? 'Resetting...'
- : 'Reset Human'}
+ ? t('prompts.resetting')
+ : t('prompts.resetHuman')}
)}
{canResetPrompt(agent.name, 'all') && (
@@ -735,8 +740,8 @@ function SettingsPrompts() {
>
{isDeleteLoading && resetOperation?.promptName === agent.name && resetOperation?.type === 'all'
- ? 'Resetting...'
- : 'Reset All'}
+ ? t('prompts.resetting')
+ : t('prompts.resetAll')}
)}
>
@@ -747,7 +752,7 @@ function SettingsPrompts() {
<>
handlePromptEdit(tool.name)}>
- Edit
+ {t('common.edit')}
{canResetPrompt(tool.name, 'tool') && (
<>
@@ -762,8 +767,8 @@ function SettingsPrompts() {
>
{isDeleteLoading && resetOperation?.promptName === tool.name && resetOperation?.type === 'tool'
- ? 'Resetting...'
- : 'Reset'}
+ ? t('prompts.resetting')
+ : t('common.reset')}
>
)}
@@ -773,7 +778,7 @@ function SettingsPrompts() {
const pageHeader = (
- }>Prompts
+ }>{t('prompts.title')}
);
@@ -785,8 +790,8 @@ function SettingsPrompts() {
>
@@ -803,7 +808,7 @@ function SettingsPrompts() {
>
@@ -824,8 +829,8 @@ function SettingsPrompts() {
-
No prompts available
-
Prompt templates could not be loaded
+
{t('prompts.noPrompts')}
+
{t('prompts.noPromptsDesc')}
@@ -843,15 +848,15 @@ function SettingsPrompts() {
-
Agent Prompts
+ {t('prompts.agentPrompts')}
{agentPrompts.length}
-
System and human prompts for AI agents
+
{t('prompts.agentPromptsDesc')}
columns={agentColumns}
data={agentPrompts}
empty={{ entityName: 'agent prompts' }}
- filterPlaceholder="Filter agents..."
+ filterPlaceholder={t('prompts.filterAgents')}
initialPageSize={1000}
renderRowContextMenu={renderAgentRowContextMenu}
renderSubComponent={renderAgentSubComponent}
@@ -864,15 +869,15 @@ function SettingsPrompts() {
-
Tool Prompts
+ {t('prompts.toolPrompts')}
{toolPrompts.length}
-
Prompt templates for system tools and utilities
+
{t('prompts.toolPromptsDesc')}
columns={toolColumns}
data={toolPrompts}
empty={{ entityName: 'tool prompts' }}
- filterPlaceholder="Filter tools..."
+ filterPlaceholder={t('prompts.filterTools')}
initialPageSize={1000}
renderRowContextMenu={renderToolRowContextMenu}
renderSubComponent={renderToolSubComponent}
@@ -883,33 +888,35 @@ function SettingsPrompts() {
}
- confirmText="Reset"
+ confirmText={t('common.reset')}
confirmVariant="destructive"
description={
resetOperation?.type === 'system'
- ? `Are you sure you want to reset the system prompt for "${resetOperation.displayName}"? This will revert it to the default template and cannot be undone.`
+ ? t('prompts.resetSystemDesc', { name: resetOperation.displayName })
: resetOperation?.type === 'human'
- ? `Are you sure you want to reset the human prompt for "${resetOperation.displayName}"? This will revert it to the default template and cannot be undone.`
+ ? t('prompts.resetHumanDesc', { name: resetOperation.displayName })
: resetOperation?.type === 'all'
- ? `Are you sure you want to reset all prompts for "${resetOperation.displayName}"? This will revert both system and human prompts to their default templates and cannot be undone.`
- : `Are you sure you want to reset the prompt for "${resetOperation?.displayName}"? This will revert it to the default template and cannot be undone.`
+ ? t('prompts.resetAllDesc', { name: resetOperation.displayName })
+ : t('prompts.resetGenericDesc', { name: resetOperation?.displayName })
}
handleConfirm={handleResetPrompt}
handleOpenChange={setResetDialogOpen}
isOpen={resetDialogOpen}
- title={`Reset ${resetOperation?.displayName || 'Prompt'}`}
+ title={t('prompts.resetPromptTitle', { name: resetOperation?.displayName || t('prompts.prompt') })}
/>
>
);
}
function SettingsPromptsHeader() {
+ const { t } = useI18n();
+
return (
-
Manage system and custom prompt templates
+
{t('prompts.managePrompts')}
);
}
diff --git a/frontend/src/pages/settings/settings-provider.tsx b/frontend/src/pages/settings/settings-provider.tsx
index 31e74ba66..7b8bf5fcc 100644
--- a/frontend/src/pages/settings/settings-provider.tsx
+++ b/frontend/src/pages/settings/settings-provider.tsx
@@ -74,6 +74,7 @@ import {
} from '@/graphql/types';
import { useAppForm } from '@/hooks/use-app-form';
import { useBreakpoint } from '@/hooks/use-breakpoint';
+import { useI18n } from '@/hooks/use-i18n';
import { routes } from '@/lib/routes';
import { cn } from '@/lib/utils';
@@ -190,6 +191,7 @@ function FormComboboxItem({
options,
placeholder,
}: FormComboboxItemProps) {
+ const { t } = useI18n();
const { field, fieldState } = useController({
control,
defaultValue: undefined,
@@ -237,13 +239,15 @@ function FormComboboxItem({
-
No {label.toLowerCase()} found.
+
+ {t('providers.noValueFound', { label: label.toLowerCase() })}
+
{search && allowCustom && (
({
size="sm"
variant="ghost"
>
- Use "{search}" as custom {label.toLowerCase()}
+ {t('providers.useCustomValue', {
+ label: label.toLowerCase(),
+ search,
+ })}
)}
@@ -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 && (
({
size="sm"
variant="ghost"
>
- Use "{search}" as custom {label.toLowerCase()}
+ {t('providers.useCustomValue', {
+ label: label.toLowerCase(),
+ search,
+ })}
)}
@@ -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')}
>
@@ -1692,30 +1738,32 @@ function SettingsProvider() {
const metaFields = (
-
{isNew ? 'Create a new provider' : 'Edit provider'}
+
+ {isNew ? t('providers.createNewProvider') : t('providers.editProviderTitle')}
+
- {isNew ? 'Configure a new language model provider' : 'Update provider settings and configuration'}
+ {isNew ? t('providers.configureNewProvider') : t('providers.updateProviderSettings')}
);
@@ -1774,7 +1822,7 @@ function SettingsProvider() {
)}
- {isAgentTestLoading && currentAgentKey === agentKey ? 'Testing...' : 'Test'}
+ {isAgentTestLoading && currentAgentKey === agentKey ? t('providers.testing') : t('common.test')}
@@ -1785,7 +1833,7 @@ function SettingsProvider() {
{
const price = option?.price;
@@ -1807,13 +1855,13 @@ function SettingsProvider() {
setValue(`agents.${agentKey}.reasoning.maxTokens` as const, null);
}}
options={availableModels}
- placeholder="Select or enter model name"
+ placeholder={t('providers.selectOrEnterModel')}
/>
-
Price Configuration
+
{t('providers.priceConfig')}
-
Extra Body
+
{t('providers.extraBodyTitle')}
@@ -1989,14 +2037,14 @@ function SettingsProvider() {
}>
- {isNew ? 'Create Provider' : 'Edit Provider'}
+ {isNew ? t('providers.createProvider') : t('providers.editProvider')}
: }
- label={isTestLoading ? 'Testing...' : 'Test'}
+ label={isTestLoading ? t('providers.testing') : t('common.test')}
onClick={() => handleTest()}
type="button"
variant="outline"
@@ -2004,7 +2052,7 @@ function SettingsProvider() {
}
- label={isNew ? 'Create' : 'Save'}
+ label={isNew ? t('common.create') : t('common.save')}
loading={isLoading}
type="submit"
/>
@@ -2012,7 +2060,7 @@ function SettingsProvider() {
{isDeleteLoading ? : }
- {isDeleteLoading ? 'Deleting...' : 'Delete'}
+ {isDeleteLoading ? t('providers.deletingProvider') : t('common.delete')}
diff --git a/frontend/src/pages/settings/settings-providers.tsx b/frontend/src/pages/settings/settings-providers.tsx
index fe0c9181d..f7cf0ef29 100644
--- a/frontend/src/pages/settings/settings-providers.tsx
+++ b/frontend/src/pages/settings/settings-providers.tsx
@@ -27,6 +27,7 @@ import {
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty';
import { Spinner } from '@/components/ui/spinner';
import { DeleteProviderDocument, ProviderType, SettingsProvidersDocument } from '@/graphql/types';
+import { useI18n } from '@/hooks/use-i18n';
import { useTableState } from '@/hooks/use-table-state';
import { routes } from '@/lib/routes';
import { formatDate } from '@/lib/utils/format';
@@ -54,6 +55,7 @@ const providerTypes = (Object.keys(providerLabels) as ProviderType[]).map((type)
}));
export function SettingsProvidersHeader() {
+ const { t } = useI18n();
const navigate = useNavigate();
// Cached: the list above already fetched this query, so the read is local.
const { data } = useQuery(SettingsProvidersDocument);
@@ -77,13 +79,13 @@ export function SettingsProvidersHeader() {
variant="secondary"
>
- Create Provider
+ {t('providers.createProvider')}
{availableTypes.length === 0 ? (
- No available provider types
+ {t('providers.noAvailableTypes')}
) : (
availableTypes.map(({ label, type }) => {
const Icon = providerIcons[type]?.icon;
@@ -105,6 +107,7 @@ export function SettingsProvidersHeader() {
}
function SettingsProviders() {
+ const { t } = useI18n();
const { data, error, loading: isLoading, refetch } = useQuery(SettingsProvidersDocument);
const [deleteProvider, { loading: isDeleteLoading }] = useMutation(DeleteProviderDocument);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
@@ -127,7 +130,7 @@ function SettingsProviders() {
setDeletingProvider(null);
} catch (error) {
- toast.error('Failed to delete provider', {
+ toast.error(t('providers.deleteFailed'), {
description: error instanceof Error ? error.message : undefined,
});
}
@@ -163,7 +166,7 @@ function SettingsProviders() {
header: ({ column }) => (
),
// Name flexes to fill remaining width — fixed `size` would push
@@ -190,7 +193,7 @@ function SettingsProviders() {
header: ({ column }) => (
),
meta: { searchable: true },
@@ -207,10 +210,10 @@ function SettingsProviders() {
header: ({ column }) => (
),
- meta: { columnMenuLabel: 'Created' },
+ meta: { columnMenuLabel: t('providers.createdAt') },
size: 120,
sortingFn: (rowA, rowB) => {
const dateA = new Date(rowA.getValue('createdAt') as string);
@@ -229,7 +232,7 @@ function SettingsProviders() {
header: ({ column }) => (
),
size: 120,
@@ -249,7 +252,7 @@ function SettingsProviders() {
@@ -262,11 +265,11 @@ function SettingsProviders() {
>
handleProviderEdit(provider.id)}>
- Edit
+ {t('common.edit')}
handleProviderClone(provider.id)}>
- Clone
+ {t('common.clone')}
- Deleting...
+ {t('providers.deletingProvider')}
>
) : (
<>
- Delete
+ {t('common.delete')}
>
)}
@@ -305,7 +308,7 @@ function SettingsProviders() {
const { agents } = provider;
if (!agents) {
- return No agent configuration available
;
+ return {t('providers.noAgentConfig')}
;
}
const getName = (key: string): string =>
@@ -338,7 +341,7 @@ function SettingsProviders() {
return (
-
Agent Configurations
+
{t('providers.agentConfigurations')}
{agentTypes.map(({ data, key, name }) => {
@@ -359,7 +362,7 @@ function SettingsProviders() {
))}
) : (
-
No configuration available
+
{t('providers.noConfig')}
)}
);
@@ -374,11 +377,11 @@ function SettingsProviders() {
<>
handleProviderEdit(provider.id)}>
- Edit
+ {t('common.edit')}
handleProviderClone(provider.id)}>
- Clone
+ {t('common.clone')}
handleProviderDeleteDialogOpen(provider)}
>
- {isDeleteLoading && deletingProvider?.id === provider.id ? 'Deleting...' : 'Delete'}
+ {isDeleteLoading && deletingProvider?.id === provider.id
+ ? t('providers.deletingProvider')
+ : t('common.delete')}
>
),
@@ -396,7 +401,7 @@ function SettingsProviders() {
const pageHeader = (
- }>Providers
+ }>{t('providers.title')}
@@ -410,8 +415,8 @@ function SettingsProviders() {
{pageHeader}
>
@@ -427,7 +432,7 @@ function SettingsProviders() {
>
@@ -446,10 +451,8 @@ function SettingsProviders() {
- No providers configured
-
- Get started by adding your first language model provider
-
+ {t('providers.noProvidersTitle')}
+ {t('providers.noProvidersDesc')}
- Add Provider
+ {t('providers.addProvider')}
@@ -474,7 +477,7 @@ function SettingsProviders() {
columns={columns}
data={providers}
empty={{ entityName: 'providers' }}
- filterPlaceholder="Filter providers..."
+ filterPlaceholder={t('providers.filterProviders')}
filterValue={filter}
onFilterChange={setFilter}
onPageChange={handlePageChange}
@@ -484,8 +487,8 @@ function SettingsProviders() {
/>
handleProviderDelete(deletingProvider?.id)}
handleOpenChange={setIsDeleteDialogOpen}
isOpen={isDeleteDialogOpen}
diff --git a/frontend/src/pages/templates/template.tsx b/frontend/src/pages/templates/template.tsx
index e1e5f4417..fba5553c7 100644
--- a/frontend/src/pages/templates/template.tsx
+++ b/frontend/src/pages/templates/template.tsx
@@ -40,196 +40,33 @@ import { useTemplateDetailNavigation } from '@/features/templates/use-template-d
import { FlowTemplateDocument } from '@/graphql/types';
import { useAppForm } from '@/hooks/use-app-form';
import { useBreakpoint } from '@/hooks/use-breakpoint';
+import { useI18n } from '@/hooks/use-i18n';
import { isNotFoundError } from '@/lib/errors';
import { routes } from '@/lib/routes';
import { cn } from '@/lib/utils';
import { type Template, useTemplates } from '@/providers/templates-provider';
-const formSchema = z.object({
- text: z.string().trim().min(1, { message: 'Text is required' }),
- title: z.string().trim().min(1, { message: 'Title is required' }),
-});
-
-type FormValues = z.infer;
-
-const PRESETS_TITLE = 'Preset templates';
-
-const PRESET_TEMPLATES: { text: string; title: string }[] = [
- {
- text: `Perform comprehensive security assessment of web application: {{TARGET_URL}}
-
-Action plan:
-1. Application Exploration: Navigate all pages, test features, identify endpoints and input vectors
-2. Vulnerability Testing per endpoint:
- - Path Traversal: attempt to read /etc/passwd, focus on file download/upload features
- - XSS: inject unique markers, scan responses, craft context-specific payloads
- - SQL Injection: run sqlmap on inputs, use tamper scripts for WAF bypass
- - Command Injection: use time-based detection, try commix utility
- - SSRF: use Interactsh for OOB, target file upload/PDF generation endpoints
- - XXE: test XML uploads and Office documents
- - Unsafe File Upload: test executable extensions, double extensions, null byte injection
- - CSRF: test token validation, POST to GET conversion
-3. Authentication & Session: test for broken authentication, session fixation, weak password policies
-4. Business Logic: identify privilege escalation, price manipulation, workflow bypass opportunities
-5. Report: document all findings with reproduction steps and proof-of-concept exploits`,
- title: 'Web Application Security Assessment',
- },
- {
- text: `Perform network infrastructure reconnaissance of target: {{TARGET_NETWORK}}
-
-Action plan:
-1. Network Discovery: identify live hosts using nmap ping sweeps, map network topology
-2. Port Scanning: comprehensive port scan (1-65535), identify all open services
-3. Service Enumeration: fingerprint service versions, detect OS information
-4. Vulnerability Scanning: run automated vulnerability scans against discovered services
-5. SSL/TLS Analysis: check certificate validity, weak ciphers, protocol vulnerabilities
-6. Banner Grabbing: collect detailed service information for exploit research
-7. Network Diagram: create visual map of discovered infrastructure
-8. Report: prioritized list of hosts, services, and potential attack vectors`,
- title: 'Network Infrastructure Discovery & Mapping',
- },
- {
- text: `Conduct Active Directory security assessment for domain: {{DOMAIN_NAME}}
-
-Action plan:
-1. Initial Access: test password spraying, check for AS-REP roasting, look for Kerberoastable accounts
-2. Domain Enumeration: enumerate users, groups, computers, GPOs, trust relationships
-3. Privilege Escalation: identify misconfigured ACLs, check for exploitable group memberships, find delegation issues
-4. Credential Harvesting: search for credentials in SYSVOL, check for password in AD attributes, dump NTDS.dit if possible
-5. Lateral Movement: test pass-the-hash, pass-the-ticket, overpass-the-hash techniques
-6. Persistence: identify opportunities for golden ticket, silver ticket, DCSync rights
-7. Domain Admin Path: map attack path from current privileges to Domain Admin
-8. Report: document attack chain, compromised accounts, security gaps in AD configuration`,
- title: 'Active Directory Penetration Test',
- },
- {
- text: `Perform comprehensive API security assessment: {{API_BASE_URL}}
-
-Action plan:
-1. API Discovery: identify all endpoints, HTTP methods, parameters
-2. Authentication Testing: test broken authentication, token manipulation, JWT vulnerabilities
-3. Authorization Testing: test broken object-level authorization (BOLA/IDOR), function-level authorization bypass
-4. Input Validation: test injection attacks (SQL, NoSQL, Command, XXE), mass assignment vulnerabilities
-5. Rate Limiting: test for absence of rate limiting, brute force protection
-6. Business Logic: test for excessive data exposure, lack of resource limiting, unsafe consumption of APIs
-7. Security Misconfiguration: check CORS policy, security headers, verbose error messages
-8. GraphQL Specific (if applicable): test introspection, query depth limits, batching attacks
-9. Report: document API vulnerabilities with curl/Postman proof-of-concepts`,
- title: 'API Security Testing',
- },
- {
- text: `Perform security audit of AWS infrastructure: {{AWS_ACCOUNT_ID or DOMAIN}}
-
-Action plan:
-1. Reconnaissance: identify S3 buckets, EC2 instances, public endpoints, enumerate services via DNS
-2. S3 Security: test bucket permissions, public access, ACL misconfigurations, bucket policies
-3. IAM Assessment: review roles, policies, check for overly permissive permissions, find unused credentials
-4. EC2 Security: scan for open security groups, test instance metadata service (169.254.169.254), check IMDSv2
-5. Network Security: review VPC configurations, security groups, NACLs, public subnets
-6. Database Exposure: check RDS public accessibility, security groups, encryption settings
-7. Lambda Functions: test for function URL exposure, environment variable leaks, IAM role permissions
-8. CloudTrail & Logging: verify logging is enabled, check for security monitoring gaps
-9. Report: prioritized cloud security findings with AWS-specific remediation steps`,
- title: 'Cloud Infrastructure Security Audit (AWS)',
- },
- {
- text: `Conduct WordPress security assessment: {{WORDPRESS_URL}}
-
-Action plan:
-1. Version Detection: identify WordPress core version, theme, and active plugins
-2. Plugin Vulnerabilities: enumerate installed plugins, check for known CVEs using WPScan and Sploitus
-3. Theme Vulnerabilities: identify theme version, search for known exploits
-4. User Enumeration: enumerate valid usernames via REST API, author archives, login responses
-5. Authentication Testing: test weak passwords, brute force protection, 2FA bypass
-6. File Upload: test media upload restrictions, arbitrary file upload vulnerabilities
-7. XML-RPC: check if enabled, test pingback SSRF, brute force amplification
-8. SQL Injection: test search functionality, custom query parameters, plugin-specific inputs
-9. XSS Testing: test comments, search, contact forms, custom fields
-10. Configuration Issues: check wp-config.php exposure, directory listing, sensitive file access
-11. Report: document WordPress-specific vulnerabilities with exploit steps`,
- title: 'WordPress Security Assessment',
- },
- {
- text: `Perform external attack surface assessment for organization: {{ORGANIZATION_NAME or DOMAIN}}
-
-Action plan:
-1. Asset Discovery: enumerate all domains, subdomains (subfinder, amass), IP ranges, ASN information
-2. Certificate Transparency: search crt.sh for subdomains, identify forgotten assets
-3. Port Scanning: scan all discovered assets for open ports and services
-4. Web Application Fingerprinting: identify technologies, CMS, frameworks, server versions
-5. Email Security: test SPF, DKIM, DMARC records, email spoofing potential
-6. Cloud Asset Discovery: search for exposed S3 buckets, Azure blobs, exposed cloud databases
-7. Sensitive Data Exposure: search GitHub, GitLab, Pastebin for leaked credentials, API keys
-8. Third-Party Integrations: identify SaaS applications, API endpoints, partner integrations
-9. Vulnerability Prioritization: identify internet-facing critical vulnerabilities
-10. Report: comprehensive external attack surface map with risk-prioritized findings`,
- title: 'External Attack Surface Assessment',
- },
- {
- text: `Conduct internal network penetration test from position: {{INITIAL_ACCESS_LEVEL}}
-
-Action plan:
-1. Network Reconnaissance: ARP scanning, identify network segments, map internal infrastructure
-2. Service Discovery: comprehensive port scanning of internal hosts, identify critical servers
-3. SMB/NetBIOS Enumeration: test null sessions, enumerate shares, check for anonymous access
-4. Credential Attacks: LLMNR/NBT-NS poisoning (Responder), relay attacks, password spraying
-5. Vulnerability Exploitation: exploit unpatched services, test default credentials, known CVEs
-6. Privilege Escalation: exploit local vulnerabilities, misconfigured services, weak permissions
-7. Lateral Movement: pass-the-hash, token impersonation, exploit trust relationships
-8. Data Exfiltration: identify sensitive data locations, test data loss prevention controls
-9. Persistence: establish persistent access mechanisms
-10. Report: document internal security posture, attack path visualization, remediation priorities`,
- title: 'Internal Network Penetration Test',
- },
- {
- text: `Perform security testing of mobile application backend API: {{API_URL}}
-
-Action plan:
-1. Traffic Interception: analyze mobile app traffic, extract API endpoints and authentication
-2. Authentication Mechanisms: test OAuth flows, JWT implementation, refresh token handling, certificate pinning bypass
-3. API Endpoint Testing: test all discovered endpoints for BOLA/IDOR, broken function-level authorization
-4. Data Validation: test for injection attacks in API parameters, test file upload endpoints
-5. Business Logic: test premium feature bypass, subscription validation, in-app purchase verification
-6. Session Management: test token expiration, concurrent session handling, session fixation
-7. Sensitive Data: check for PII exposure, excessive data in responses, hardcoded secrets
-8. Rate Limiting: test brute force protection on login, API rate limits, account lockout
-9. Deep Linking: test for deep link hijacking, intent redirection (Android), URL scheme abuse (iOS)
-10. Report: mobile-specific vulnerabilities with mitigation recommendations`,
- title: 'Mobile Application Security Testing (API Backend)',
- },
- {
- text: `Assess DevOps infrastructure and CI/CD pipeline security: {{ORGANIZATION}}
-
-Action plan:
-1. Repository Security: scan GitHub/GitLab for exposed secrets, API keys, credentials in commit history
-2. CI/CD Configuration: review Jenkins/GitLab CI/GitHub Actions configurations, test for injection in pipeline definitions
-3. Container Security: scan Docker images for vulnerabilities, test for container escape, check image sources
-4. Secrets Management: test secret storage (HashiCorp Vault, AWS Secrets Manager), check for hardcoded secrets
-5. Access Control: review permissions on repositories, pipeline access, deployment keys, service accounts
-6. Artifact Security: scan build artifacts, test artifact repository access controls (Nexus, Artifactory)
-7. Kubernetes Security: review pod security policies, RBAC, network policies, exposed dashboards
-8. Infrastructure as Code: review Terraform/Ansible for misconfigurations, overly permissive IAM roles
-9. Monitoring & Logging: verify security logging, test log tampering, check for security monitoring gaps
-10. Report: DevOps security findings with secure pipeline recommendations`,
- title: 'DevOps & CI/CD Pipeline Security',
- },
- {
- text: `Conduct database security assessment: {{DATABASE_TYPE}} at {{HOST:PORT}}
-
-Action plan:
-1. Access Testing: test for default credentials, weak passwords, anonymous access
-2. Network Exposure: verify database should not be internet-accessible, check firewall rules
-3. Authentication: test authentication mechanisms, user enumeration, password policies
-4. Authorization: review user permissions, test for privilege escalation, check for excessive grants
-5. Injection Testing: SQL injection in application layer, test stored procedures for injection
-6. Configuration Review: check for dangerous configuration options (xp_cmdshell, LOAD DATA, file_priv)
-7. Encryption: verify data-at-rest encryption, SSL/TLS for connections, check for sensitive data in plaintext
-8. Backup Security: test backup file access, check backup encryption, verify backup restoration procedures
-9. Audit Logging: verify audit logs enabled, test log tampering, check retention policies
-10. Report: database-specific security findings with hardening recommendations`,
- title: 'Database Security Assessment',
- },
-];
+type FormValues = z.infer>;
+
+const buildFormSchema = (requiredMessage: string) =>
+ z.object({
+ text: z.string().trim().min(1, { message: requiredMessage }),
+ title: z.string().trim().min(1, { message: requiredMessage }),
+ });
+
+const PRESET_KEYS = [
+ 'presetWebApp',
+ 'presetNetwork',
+ 'presetAd',
+ 'presetApi',
+ 'presetAws',
+ 'presetWordpress',
+ 'presetExternal',
+ 'presetInternal',
+ 'presetMobile',
+ 'presetDevOps',
+ 'presetDatabase',
+] as const;
const renderTemplateItem = (item: Template, isCurrent: boolean): ReactNode => (
{item.title}
@@ -251,12 +88,24 @@ function Template() {
}
function TemplateForm({ templateId }: { templateId?: string }) {
+ const { t } = useI18n();
const navigate = useNavigate();
const { createTemplate, deleteTemplate, updateTemplate } = useTemplates();
const { isDesktop, isMobile } = useBreakpoint();
const isNew = templateId === 'new';
+ const formSchema = useMemo(() => buildFormSchema(t('errors.required')), [t]);
+
+ const PRESET_TEMPLATES = useMemo<{ text: string; title: string }[]>(
+ () =>
+ PRESET_KEYS.map((key) => ({
+ text: t(`templates.${key}Text`),
+ title: t(`templates.${key}Title`),
+ })),
+ [t],
+ );
+
const templateNav = useTemplateDetailNavigation(isNew ? null : templateId);
const [expandedPresetIndex, setExpandedPresetIndex] = useState(null);
@@ -333,14 +182,14 @@ function TemplateForm({ templateId }: { templateId?: string }) {
// Send the server's current `text`, not the form's, so renaming the title never persists the
// user's unsaved body edits — those stay dirty in the form (kept by `keepDirtyValues`) until they save.
await updateTemplate(templateId, { text: template.text, title: newTitle });
- toast.success('Template renamed successfully');
+ toast.success(t('templates.renamedSuccess'));
handleTemplateRenameCancel();
} catch {
// Error already handled in provider with toast
} finally {
setIsRenaming(false);
}
- }, [editingInputRef, handleTemplateRenameCancel, templateId, templateData?.flowTemplate, updateTemplate]);
+ }, [editingInputRef, handleTemplateRenameCancel, templateId, templateData?.flowTemplate, updateTemplate, t]);
const handleTemplateDelete = useCallback(async () => {
if (!templateId) {
@@ -392,7 +241,7 @@ function TemplateForm({ templateId }: { templateId?: string }) {
const parsed = formSchema.safeParse(getValues());
return parsed.success ? performSave(parsed.data) : false;
- }, [getValues, isSaving, isValid, performSave]);
+ }, [getValues, isSaving, isValid, performSave, formSchema]);
const guard = useUnsavedChangesGuard({
isDirty,
@@ -456,7 +305,7 @@ function TemplateForm({ templateId }: { templateId?: string }) {
inputRef={editingInputRef}
onCancel={handleTemplateRenameCancel}
onSave={handleTemplateRenameSave}
- placeholder="Template title"
+ placeholder={t('templates.titlePlaceholder')}
/>
) : hasTemplate ? (
@@ -465,14 +314,16 @@ function TemplateForm({ templateId }: { templateId?: string }) {
className="max-w-64 min-w-0 cursor-text truncate select-none"
onDoubleClick={handleTemplateRenameStart}
>
- {templateName ?? 'Template'}
+ {templateName ?? t('templates.templateFallback')}
- Double-click to rename
+ {t('templates.doubleClickRename')}
) : (
- {isNew ? 'New template' : (templateName ?? 'Template')}
+ {isNew
+ ? t('templates.newTemplateBreadcrumb')
+ : (templateName ?? t('templates.templateFallback'))}
)}
@@ -486,21 +337,21 @@ function TemplateForm({ templateId }: { templateId?: string }) {
controller={templateNav}
renderItem={renderTemplateItem}
sheetIcon={}
- sheetTitle="Templates"
+ sheetTitle={t('templates.templatesSheetTitle')}
/>
)}
}
- label={isNew ? 'Create' : 'Save'}
+ label={isNew ? t('common.create') : t('common.save')}
loading={isSaving}
type="submit"
/>
@@ -521,11 +372,11 @@ function TemplateForm({ templateId }: { templateId?: string }) {
onSelect={(event) => event.preventDefault()}
>
- Templates
+ {t('templates.templatesSheetTitle')}
controller={templateNav}
- sheetTitle="Templates"
+ sheetTitle={t('templates.templatesSheetTitle')}
size="sm"
/>
@@ -538,7 +389,7 @@ function TemplateForm({ templateId }: { templateId?: string }) {
onClick={handleTemplateRenameStart}
>
- Rename
+ {t('common.rename')}
>
@@ -547,12 +398,12 @@ function TemplateForm({ templateId }: { templateId?: string }) {
className="cursor-default gap-4 hover:bg-transparent focus:bg-transparent"
onSelect={(event) => event.preventDefault()}
>
- View
+ {t('templates.view')}
{!isNew && (
@@ -565,12 +416,12 @@ function TemplateForm({ templateId }: { templateId?: string }) {
{isDeleting ? (
<>
- Deleting...
+ {t('templates.deleting')}
>
) : (
<>
- Delete
+ {t('common.delete')}
>
)}
@@ -586,7 +437,7 @@ function TemplateForm({ templateId }: { templateId?: string }) {
controller={templateNav}
renderItem={renderTemplateItem}
sheetIcon={}
- sheetTitle="Templates"
+ sheetTitle={t('templates.templatesSheetTitle')}
/>
)}
>
@@ -652,7 +503,7 @@ function TemplateForm({ templateId }: { templateId?: string }) {
- {PRESETS_TITLE}
+ {t('templates.presetsTitle')}
-
- Click a preset to fill the form, or expand it to preview the content.
-
+
{t('templates.presetsHint')}
{presetsList()}
@@ -678,7 +527,7 @@ function TemplateForm({ templateId }: { templateId?: string }) {
variant="secondary"
>
- {PRESETS_TITLE}
+ {t('templates.presetsTitle')}
- {isNew ? 'Create a new template' : 'Edit template'}
- Add a title and content, or start from a preset.
+
+ {isNew ? t('templates.createTitle') : t('templates.editTitle')}
+
+ {t('templates.introDesc')}
);
@@ -709,12 +560,12 @@ function TemplateForm({ templateId }: { templateId?: string }) {
name="title"
render={({ field }) => (
- Title
+ {t('common.title')}
@@ -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.
- navigate(routes.templates)}>Back to Templates
+ {t('templates.notFoundTitle')}
+ {t('templates.notFoundDesc')}
+ navigate(routes.templates)}>{t('templates.backToTemplates')}
@@ -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() {
e.stopPropagation()}
variant="ghost"
@@ -193,11 +195,11 @@ function Templates() {
>
handleTemplateOpen(template.id)}>
- Edit
+ {t('common.edit')}
handleTemplateRenameStart(template)}>
- Rename
+ {t('common.rename')}
- Deleting...
+ {t('templates.deleting')}
>
) : (
<>
- Delete
+ {t('common.delete')}
>
)}
@@ -233,11 +235,11 @@ function Templates() {
<>
handleTemplateOpen(template.id)}>
- Edit
+ {t('common.edit')}
handleTemplateRenameStart(template)}>
- Rename
+ {t('common.rename')}
handleDeleteDialogOpen(template)}
>
- {deletingIds.has(template.id) ? 'Deleting...' : 'Delete'}
+ {deletingIds.has(template.id) ? t('templates.deleting') : t('common.delete')}
>
);
@@ -253,12 +255,12 @@ function Templates() {
const pageHeader = (
- }>Templates
+ }>{t('templates.title')}
}
- label="New Template"
+ label={t('templates.newTemplate')}
onClick={() => navigate(routes.newTemplate)}
variant="secondary"
/>
@@ -272,8 +274,8 @@ function Templates() {
{pageHeader}
>
@@ -289,7 +291,7 @@ function Templates() {
>
@@ -306,8 +308,8 @@ function Templates() {
-
No templates yet
-
Create your first template to get started
+
{t('templates.noTemplatesTitle')}
+
{t('templates.noTemplatesDesc')}
- New Template
+ {t('templates.newTemplate')}
@@ -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() {
/>