1836 lines
122 KiB
JavaScript
1836 lines
122 KiB
JavaScript
/* ═══════════════════════════════════════════════
|
||
Crank — i18n
|
||
Reads `crank_lang` from localStorage ('en'|'ru')
|
||
═══════════════════════════════════════════════ */
|
||
|
||
var TRANSLATIONS = {
|
||
en: {
|
||
// Nav
|
||
'nav.operations': 'Operations',
|
||
'nav.agents': 'Agents',
|
||
'nav.apikeys': 'API Keys',
|
||
'nav.secrets': 'Secrets',
|
||
'nav.logs': 'Logs',
|
||
'nav.usage': 'Usage',
|
||
'nav.settings': 'Settings',
|
||
'nav.logout': 'Log out',
|
||
'nav.workspaces': 'Your workspaces',
|
||
'nav.menu': 'Menu',
|
||
'nav.account': 'Account',
|
||
'nav.user_fallback': 'Crank',
|
||
'nav.workspace_fallback': 'workspace',
|
||
|
||
// Operations page
|
||
'ops.title': 'Operations',
|
||
'ops.subtitle': 'Catalog of tools. List of created MCP tools for API endpoints.',
|
||
'ops.new': 'New operation',
|
||
'ops.loading': 'Loading operations…',
|
||
'ops.empty.title': 'No operations found',
|
||
'ops.empty.sub': 'Try adjusting your search or filters.',
|
||
'ops.empty.initial.title': 'No operations yet',
|
||
'ops.empty.initial.sub': 'Create the first operation in this workspace to expose a live MCP tool.',
|
||
'ops.error.title': 'Backend connection failed',
|
||
'ops.delete.confirm': 'Delete this operation? This cannot be undone.',
|
||
'ops.delete.success.title': 'Operation deleted',
|
||
'ops.delete.success.message': '{name} was deleted.',
|
||
'ops.delete.error.title': 'Delete failed',
|
||
'ops.delete.error.message': 'Failed to delete operation',
|
||
'ops.action.edit': 'Edit operation',
|
||
'ops.action.delete':'Delete operation',
|
||
'ops.stats.synced': 'Catalog synced with backend',
|
||
'ops.stats.none': 'No operations yet',
|
||
'ops.stats.share': '{value}% of total',
|
||
'ops.stats.no_published': 'No published tools yet',
|
||
'ops.stats.requests_rollup': 'Summed from operation usage',
|
||
'ops.stats.no_traffic': 'No traffic recorded today',
|
||
'ops.stats.latency_weighted': "Weighted by today's calls",
|
||
'ops.stats.latency_waiting': 'Waiting for runtime samples',
|
||
'ops.results': 'Showing {shown} of {total}',
|
||
'ops.range': 'Showing {from}–{to} of {total} operations',
|
||
'ops.filter.category': 'Category: {value}',
|
||
'ops.filter.agent': 'Agent: {value}',
|
||
'ops.no_active_agents': 'No active agents',
|
||
|
||
// Stats
|
||
'stats.total': 'Total operations',
|
||
'stats.active': 'Active',
|
||
'stats.requests': 'Requests today',
|
||
'stats.latency': 'Avg. latency',
|
||
|
||
// Tabs
|
||
'tab.all': 'All',
|
||
'tab.active': 'Active',
|
||
'tab.draft': 'Draft',
|
||
'tab.error': 'Error',
|
||
'tab.inactive': 'Inactive',
|
||
|
||
// Filters & sort
|
||
'filter.search': 'Search operations...',
|
||
'filter.status': 'Status',
|
||
'filter.category': 'Category',
|
||
'filter.agent': 'Agent',
|
||
'filter.showing': 'Showing',
|
||
'filter.of': 'of',
|
||
'filter.clear': 'Clear filter',
|
||
'filter.clear_all': 'Clear all',
|
||
'filter.remove': 'Remove filter',
|
||
'sort.created_desc':'Sort: Last created',
|
||
'sort.created_asc': 'Sort: Oldest first',
|
||
'sort.name_asc': 'Sort: Name A–Z',
|
||
'sort.name_desc': 'Sort: Name Z–A',
|
||
|
||
// Table headers
|
||
'th.name': 'NAME',
|
||
'th.protocol': 'PROTOCOL',
|
||
'th.status': 'STATUS',
|
||
'th.created': 'CREATED',
|
||
'th.url': 'TARGET URL',
|
||
|
||
// Pagination
|
||
'page.showing': 'Showing',
|
||
'page.of': 'of',
|
||
'page.ops': 'operations',
|
||
|
||
// API Keys page
|
||
'apikeys.title': 'Agent Keys',
|
||
'apikeys.subtitle': 'These keys connect an MCP client to the MCP server and are issued for a specific agent.',
|
||
'apikeys.new': 'Create key',
|
||
'apikeys.new_mcp': 'Create MCP client key',
|
||
'apikeys.new_approval': 'Create approval key',
|
||
'apikeys.kind.mcp': 'MCP clients',
|
||
'apikeys.kind.approval': 'Approvals',
|
||
'apikeys.kind.mcp_hint': 'MCP client keys connect an agent to tools/list and tools/call.',
|
||
'apikeys.kind.approval_hint': 'Approval keys are used only by an external human confirmation interface.',
|
||
'apikeys.agent.title': 'Agent selection',
|
||
'apikeys.agent.subtitle': 'Select the AI agent this key is issued for.',
|
||
'apikeys.agent.label': 'AI agent',
|
||
'apikeys.agent.empty': 'No agents available',
|
||
'apikeys.agent.empty_hint': 'Create an AI agent first, then issue machine access keys for its MCP endpoint.',
|
||
'apikeys.agent.endpoint': 'MCP endpoint: {endpoint}',
|
||
'apikeys.agent.endpoint_missing': 'The selected agent does not have a published MCP endpoint yet.',
|
||
'apikeys.th.name': 'NAME',
|
||
'apikeys.th.scope': 'ACCESS',
|
||
'apikeys.th.created':'CREATED',
|
||
'apikeys.th.last': 'LAST USED',
|
||
'apikeys.callout.title': 'Keys are only shown once.',
|
||
'apikeys.callout.body': 'Copy and store it securely right after creation — Crank stores only a hash. Last used updates after a successful MCP call.',
|
||
'apikeys.active.title': 'Active keys',
|
||
'apikeys.active.subtitle': '{active} active · {revoked} revoked',
|
||
'apikeys.search': 'Filter keys…',
|
||
'apikeys.th.prefix': 'KEY PREFIX',
|
||
'apikeys.th.scopes': 'ACCESS',
|
||
'apikeys.th.status': 'STATUS',
|
||
'apikeys.scope_ref': 'Access reference',
|
||
'apikeys.scope.read': 'Initialize MCP sessions, ping the server, and list tools for the selected AI agent.',
|
||
'apikeys.scope.write': 'Execute `tools/call` requests against published agent toolsets.',
|
||
'apikeys.scope.deploy': 'Reserved for deploy-scoped automation. Today it also permits MCP read/write flows.',
|
||
'apikeys.scope.approve': 'Confirm a pending human approval request for this agent.',
|
||
'apikeys.scope.deny': 'Reject a pending human approval request for this agent.',
|
||
'apikeys.modal.title': 'Create agent key',
|
||
'apikeys.modal.title_mcp': 'Create MCP client key',
|
||
'apikeys.modal.title_approval': 'Create approval key',
|
||
'apikeys.modal.name': 'Key name',
|
||
'apikeys.modal.name_hint': 'A descriptive label to identify the key. Only visible to admins.',
|
||
'apikeys.modal.name_placeholder': 'e.g. Production, CI pipeline',
|
||
'apikeys.modal.scopes': 'Access',
|
||
'apikeys.modal.reveal_title': 'Copy this key now.',
|
||
'apikeys.modal.reveal_body': "It won't be shown again.",
|
||
'apikeys.modal.copy': 'Copy to clipboard',
|
||
'apikeys.modal.cancel': 'Cancel',
|
||
'apikeys.modal.create': 'Create key',
|
||
'apikeys.modal.done': "Done — I've copied the key",
|
||
'apikeys.status.active': 'Active',
|
||
'apikeys.status.revoked': 'Revoked',
|
||
'apikeys.last_used.never': 'Never',
|
||
'apikeys.empty.none': 'No API keys yet',
|
||
'apikeys.empty.approval': 'No approval keys yet. Create one only if this agent has tools that require human confirmation.',
|
||
'apikeys.empty.search': 'No keys match your search',
|
||
'apikeys.loading': 'Loading…',
|
||
'apikeys.error.api': 'Workspace or API is unavailable',
|
||
'apikeys.error.load': 'Failed to load API keys',
|
||
'apikeys.confirm.revoke': 'Revoke this key? All requests using it will fail immediately.',
|
||
'apikeys.confirm.delete': 'Delete this key? This cannot be undone.',
|
||
'apikeys.toast.revoke_title': 'Key revoked',
|
||
'apikeys.toast.revoke_message': '{name} was revoked.',
|
||
'apikeys.toast.revoke_error_title': 'Revoke failed',
|
||
'apikeys.toast.revoke_error_message': 'Failed to revoke key',
|
||
'apikeys.toast.delete_title': 'Key deleted',
|
||
'apikeys.toast.delete_message': '{name} was deleted.',
|
||
'apikeys.toast.delete_error_title': 'Delete failed',
|
||
'apikeys.toast.delete_error_message': 'Failed to delete key',
|
||
'apikeys.toast.scope_missing_title': 'No access selected',
|
||
'apikeys.toast.scope_missing_message': 'Select at least one access level before creating a key.',
|
||
'apikeys.toast.create_title': 'Agent key created',
|
||
'apikeys.toast.create_message': 'Copy the key now. It will not be shown again.',
|
||
'apikeys.toast.create_error_title': 'Key creation failed',
|
||
'apikeys.toast.create_error_message': 'Failed to create key',
|
||
'apikeys.toast.copy_title': 'Agent key copied',
|
||
'apikeys.toast.copy_message': 'Store the raw key securely. It cannot be revealed again.',
|
||
'apikeys.toast.prefix_title': 'Key prefix copied',
|
||
'apikeys.action.copy_prefix': 'Copy key prefix',
|
||
'apikeys.action.revoke': 'Revoke key',
|
||
'apikeys.action.delete': 'Delete',
|
||
'apikeys.creating': 'Creating…',
|
||
'apikeys.approval.warning': 'Do not pass this key to an LLM or MCP client. It is only for an external interface where a human confirms an action.',
|
||
|
||
// Secrets page
|
||
'secrets.title': 'Secrets',
|
||
'secrets.subtitle': 'Credentials for authorization on target API hosts.',
|
||
'secrets.new': 'Create secret',
|
||
'secrets.callout.title': 'Secret values are protected.',
|
||
'secrets.callout.body': 'After saving, secrets are encrypted with the workspace key and cannot be read back. A secret can be rotated or deleted.',
|
||
'secrets.active.title': 'Workspace secrets',
|
||
'secrets.active.subtitle': '{active} active · {total} total',
|
||
'secrets.search': 'Filter secrets…',
|
||
'secrets.th.name': 'NAME',
|
||
'secrets.th.kind': 'KIND',
|
||
'secrets.th.version': 'VERSION',
|
||
'secrets.th.last_used': 'LAST USED',
|
||
'secrets.th.used_by': 'USED BY',
|
||
'secrets.th.status': 'STATUS',
|
||
'secrets.kind.token': 'Token',
|
||
'secrets.kind.username_password': 'Username/password',
|
||
'secrets.kind.header': 'Header value',
|
||
'secrets.kind.generic': 'Generic JSON',
|
||
'secrets.status.active': 'Active',
|
||
'secrets.status.disabled': 'Disabled',
|
||
'secrets.last_used.never': 'Never',
|
||
'secrets.used_by_count.one': '{count} binding',
|
||
'secrets.used_by_count.other': '{count} bindings',
|
||
'secrets.loading': 'Loading…',
|
||
'secrets.empty.none': 'No secrets yet',
|
||
'secrets.empty.search': 'No secrets match your search',
|
||
'secrets.error.workspace': 'Workspace is not selected',
|
||
'secrets.error.load': 'Failed to load secrets',
|
||
'secrets.action.rotate': 'Rotate',
|
||
'secrets.action.delete': 'Delete',
|
||
'secrets.confirm.delete': 'Delete secret {name}? This cannot be undone while the secret is still used by an operation.',
|
||
'secrets.toast.create_title': 'Secret created',
|
||
'secrets.toast.create_message': 'The secret was stored encrypted and cannot be viewed again.',
|
||
'secrets.toast.create_error_title': 'Secret creation failed',
|
||
'secrets.toast.create_error_message': 'Failed to create secret',
|
||
'secrets.toast.rotate_title': 'Secret rotated',
|
||
'secrets.toast.rotate_message': 'A new secret version is now active.',
|
||
'secrets.toast.rotate_error_title': 'Secret rotation failed',
|
||
'secrets.toast.rotate_error_message': 'Failed to rotate secret',
|
||
'secrets.toast.delete_title': 'Secret deleted',
|
||
'secrets.toast.delete_message': '{name} was deleted.',
|
||
'secrets.toast.delete_error_title': 'Secret deletion failed',
|
||
'secrets.toast.delete_error_message': 'Failed to delete secret',
|
||
'secrets.modal.create_title': 'Create secret',
|
||
'secrets.modal.rotate_title': 'Rotate secret',
|
||
'secrets.modal.name': 'Secret name',
|
||
'secrets.modal.name_hint': 'Use a clear name so you can select this secret in operation settings later.',
|
||
'secrets.modal.kind': 'Secret kind',
|
||
'secrets.modal.value': 'Token value',
|
||
'secrets.modal.header_value': 'Header value',
|
||
'secrets.modal.username': 'Username',
|
||
'secrets.modal.password': 'Password',
|
||
'secrets.modal.json_payload': 'JSON',
|
||
'secrets.modal.json_hint': 'Enter valid JSON. Use this type when an API needs several related values.',
|
||
'secrets.modal.create_note': 'After saving, the value is encrypted and will no longer be displayed.',
|
||
'secrets.modal.rotate_note': 'Rotation replaces the stored value with a new encrypted version.',
|
||
'secrets.modal.cancel': 'Cancel',
|
||
'secrets.modal.create_action': 'Create secret',
|
||
'secrets.modal.rotate_action': 'Rotate secret',
|
||
'secrets.modal.creating': 'Creating…',
|
||
'secrets.modal.rotating': 'Rotating…',
|
||
'secrets.hint.token': 'For bearer tokens, API keys, or another single token value.',
|
||
'secrets.hint.username_password': 'For username and password authorization.',
|
||
'secrets.hint.header': 'For a value passed in an HTTP header.',
|
||
'secrets.hint.generic': 'For structured JSON credentials.',
|
||
'secrets.validation.name_required': 'Secret name is required.',
|
||
'secrets.validation.value_required': 'Secret value is required.',
|
||
'secrets.validation.username_password_required': 'Username and password are both required.',
|
||
'secrets.validation.json_invalid': 'The JSON value must be valid.',
|
||
// Logs page
|
||
'logs.title': 'Logs',
|
||
'logs.subtitle': 'Invocation log for all operations in this workspace.',
|
||
'logs.search': 'Search logs...',
|
||
'logs.level.all': 'All',
|
||
'logs.level.info': 'Info',
|
||
'logs.level.warn': 'Warn',
|
||
'logs.level.error': 'Error',
|
||
'logs.level.debug': 'Debug',
|
||
'logs.live': 'Live',
|
||
'logs.paused': 'Paused',
|
||
'logs.refresh': 'Refresh',
|
||
'logs.range.30m': 'Last 30 min',
|
||
'logs.range.1h': 'Last hour',
|
||
'logs.range.6h': 'Last 6 hours',
|
||
'logs.range.24h': 'Last 24 hours',
|
||
'logs.range.7d': 'Last 7 days',
|
||
'logs.search_messages': 'Search messages…',
|
||
'logs.detail.agent': 'Agent',
|
||
'logs.detail.request': 'Request preview',
|
||
'logs.detail.response': 'Response preview',
|
||
'logs.detail.meta': 'Metadata',
|
||
'logs.loading.title': 'Loading logs…',
|
||
'logs.loading.sub': 'Fetching invocation history for the current workspace.',
|
||
'logs.error.title': 'Unable to load logs',
|
||
'logs.error.api': 'Admin API is not available',
|
||
'logs.error.workspace': 'Workspace is not selected',
|
||
'logs.error.load': 'Failed to load logs',
|
||
'logs.empty.title': 'No log entries found',
|
||
'logs.empty.filtered': 'Try widening the period or clearing the current search and level filters.',
|
||
'logs.empty.initial': 'Run tests or published MCP tools to start collecting invocation history.',
|
||
'logs.live.on.title': 'Live mode enabled',
|
||
'logs.live.on.body': 'Polling logs every 4 seconds.',
|
||
'logs.live.off.title': 'Live mode paused',
|
||
'logs.live.off.body': 'Automatic polling is paused.',
|
||
'logs.refresh.title': 'Logs refreshed',
|
||
'logs.refresh.body': 'The latest invocation records were loaded for the current workspace.',
|
||
|
||
// Usage page
|
||
'usage.title': 'Usage',
|
||
'usage.subtitle': 'Invocation metrics for all operations in your workspace.',
|
||
'usage.export': 'Export CSV',
|
||
'usage.period.7d': 'Last 7 days',
|
||
'usage.period.30d': 'Last 30 days',
|
||
'usage.period.90d': 'Last 90 days',
|
||
'usage.period.this_month': 'This month',
|
||
'usage.period.label.7d': 'last 7 days',
|
||
'usage.period.label.30d': 'last 30 days',
|
||
'usage.period.label.90d': 'last 90 days',
|
||
'usage.period.label.this_month': 'this month',
|
||
'usage.stats.total': 'Total invocations',
|
||
'usage.stats.success': 'Success rate',
|
||
'usage.stats.p50': 'Median latency (p50)',
|
||
'usage.stats.p99': 'p99 latency',
|
||
'usage.stats.across': 'Across {period}',
|
||
'usage.stats.success_calls': '{count} successful calls',
|
||
'usage.stats.median': 'Median latency for selected period',
|
||
'usage.stats.error_calls': '{count} error calls',
|
||
'usage.chart.title': 'Invocations over time',
|
||
'usage.chart.success': 'Success',
|
||
'usage.chart.error': 'Error',
|
||
'usage.chart.empty.title': 'No usage data yet',
|
||
'usage.chart.empty.sub': 'Invocation metrics will appear here after tests or published tool calls in the selected period.',
|
||
'usage.table.title': 'By operation',
|
||
'usage.table.subtitle': 'Breakdown for {period}',
|
||
'usage.table.th.operation': 'Operation',
|
||
'usage.table.th.protocol': 'Protocol',
|
||
'usage.table.th.calls': 'Calls',
|
||
'usage.table.th.errors': 'Errors',
|
||
'usage.table.th.error_rate': 'Error rate',
|
||
'usage.table.th.latency': 'Latency (p50 / p95 / p99)',
|
||
'usage.table.th.share': 'Traffic share',
|
||
'usage.table.empty.title': 'No operation breakdown yet',
|
||
'usage.table.empty.sub': 'The selected period has no invocation samples to break down by operation.',
|
||
'usage.table.share': '{value}% of workspace traffic',
|
||
'usage.loading.title': 'Loading usage…',
|
||
'usage.loading.sub': 'Aggregating invocation metrics for the selected workspace.',
|
||
'usage.error.title': 'Unable to load usage',
|
||
'usage.error.api': 'Admin API is not available',
|
||
'usage.error.workspace': 'Workspace is not selected',
|
||
'usage.error.load': 'Failed to load usage',
|
||
'usage.export.empty.title': 'No usage data loaded',
|
||
'usage.export.empty.body': 'Load usage data before exporting the CSV snapshot.',
|
||
'usage.export.done.title': 'Usage exported',
|
||
'usage.export.done.body': 'The current usage snapshot was exported as CSV.',
|
||
'usage.chart.ok': '{count} ok',
|
||
'usage.chart.errors': '{count} errors',
|
||
'usage.chart.week': 'Wk {index}',
|
||
'usage.csv.header': 'Operation,Protocol,Calls,Errors,Error Rate (%),p50 (ms),p95 (ms),p99 (ms)',
|
||
|
||
// Settings sidebar
|
||
'settings.title': 'Settings',
|
||
'settings.nav.workspace': 'Workspace',
|
||
'settings.nav.profile': 'Profile',
|
||
'settings.nav.security': 'Security',
|
||
'settings.nav.danger': 'Danger zone',
|
||
|
||
// Settings — workspace section
|
||
'settings.ws.title': 'Workspace settings',
|
||
'settings.ws.subtitle': 'General information about your workspace',
|
||
'settings.ws.name': 'Workspace name',
|
||
'settings.ws.display': 'Display name',
|
||
'settings.ws.desc': 'Description',
|
||
|
||
// Settings — language
|
||
'settings.lang.title': 'Language',
|
||
'settings.lang.subtitle': 'Interface display language',
|
||
'settings.lang.en': 'English',
|
||
'settings.lang.ru': 'Русский',
|
||
|
||
// Settings — profile section
|
||
'settings.profile.title': 'Profile',
|
||
'settings.profile.name': 'Full name',
|
||
'settings.profile.email': 'Email',
|
||
'settings.profile.role': 'Role',
|
||
'settings.page.title': 'Account settings',
|
||
'settings.ws.slug_hint': 'Used in API endpoints and in the interface. Only lowercase English letters, numbers and hyphens are allowed.',
|
||
'settings.ws.description_optional': 'Description (optional)',
|
||
'settings.ws.description_placeholder': 'What tasks does this workspace perform?',
|
||
'settings.profile.avatar_hint': 'Your avatar is generated from your display name and email.',
|
||
'settings.profile.first_name': 'First name',
|
||
'settings.profile.last_name': 'Last name',
|
||
'settings.profile.last_name_placeholder': 'Optional',
|
||
'settings.profile.email_label': 'Email address',
|
||
'settings.profile.email_hint': 'Changing email updates the login identity for the current account.',
|
||
'settings.profile.save': 'Save profile',
|
||
'settings.profile.saving': 'Saving…',
|
||
'settings.profile.saved': 'Saved ✓',
|
||
'settings.profile.saved_status': 'Profile saved.',
|
||
'settings.profile.save_error': 'Failed to save profile',
|
||
'settings.security.title': 'Security',
|
||
'settings.security.current_password': 'Current password',
|
||
'settings.security.new_password': 'New password',
|
||
'settings.security.new_password_placeholder': 'min. 12 characters',
|
||
'settings.security.confirm_password': 'Confirm new password',
|
||
'settings.security.change_password': 'Change password',
|
||
'settings.security.mismatch': 'New password and confirmation do not match.',
|
||
'settings.security.saved': 'Password updated.',
|
||
'settings.security.save_error': 'Failed to change password',
|
||
'settings.capability.rest': 'REST / HTTP',
|
||
'settings.capability.standard': 'standard',
|
||
'settings.capability.elevated': 'elevated',
|
||
'settings.capability.strict': 'strict',
|
||
'settings.capability.static_agent_key': 'static agent keys',
|
||
'settings.capability.unlimited': 'unlimited',
|
||
'settings.capability.none': 'none',
|
||
'settings.session.title': 'Current session',
|
||
'settings.session.browser': 'Browser session',
|
||
'settings.session.current': 'current',
|
||
'settings.session.loading': 'Loading current session details…',
|
||
'settings.session.unavailable': 'Session details are unavailable. Use Log out to revoke the current browser session.',
|
||
'settings.session.current_workspace': 'current workspace',
|
||
'settings.session.summary': '{email} · {role} in {workspace}. Use Log out to revoke the current browser session.',
|
||
'settings.planned_badge': 'Planned',
|
||
'settings.workspace.saving': 'Saving…',
|
||
'settings.workspace.saved': 'Saved ✓',
|
||
'settings.workspace.save_error': 'Failed to save workspace settings',
|
||
'settings.workspace.save_error_title': 'Workspace save failed',
|
||
|
||
// Workspace setup
|
||
'workspace_setup.title': 'Workspace settings',
|
||
'workspace_setup.page_subtitle': 'Configure the identity of your self-hosted workspace.',
|
||
'workspace_setup.back': 'Back',
|
||
'workspace_setup.identity.title': 'Workspace identity',
|
||
'workspace_setup.identity.avatar_hint': 'Avatar is derived from your workspace name.',
|
||
'workspace_setup.name': 'Workspace name',
|
||
'workspace_setup.slug': 'Slug',
|
||
'workspace_setup.required': 'required',
|
||
'workspace_setup.optional': '(optional)',
|
||
'workspace_setup.slug_hint': 'Only lowercase letters, numbers, and hyphens. Used in MCP endpoint URLs.',
|
||
'workspace_setup.description': 'Description',
|
||
'workspace_setup.description_placeholder': 'What tasks does this workspace perform?',
|
||
'workspace_setup.actions.save': 'Save changes',
|
||
'workspace_setup.create.title': 'Create a new workspace',
|
||
'workspace_setup.create.subtitle': 'This Community installation uses one workspace for MCP operations and agents.',
|
||
'workspace_setup.create.footer': 'This Community installation uses one workspace.',
|
||
'workspace_setup.danger.title': 'Danger zone',
|
||
'workspace_setup.danger.export_title': 'Export all data',
|
||
'workspace_setup.danger.export_body': 'Download a JSON snapshot of workspace settings, operations, agents, secrets, usage data and agent access keys.',
|
||
'workspace_setup.export': 'Export',
|
||
'workspace_setup.role.owner': 'Owner',
|
||
'workspace_setup.role.admin': 'Admin',
|
||
'workspace_setup.role.operator': 'Developer',
|
||
'workspace_setup.role.viewer': 'Viewer',
|
||
'workspace_setup.saving_create': 'Creating…',
|
||
'workspace_setup.saving_update': 'Saving…',
|
||
'workspace_setup.saved': 'Saved ✓',
|
||
'workspace_setup.save_error': 'Failed to save workspace',
|
||
'workspace_setup.save_error_title': 'Save failed',
|
||
'workspace_setup.export_error': 'Failed to export workspace',
|
||
'workspace_setup.export_error_title': 'Export failed',
|
||
'workspace.switch_error': 'Failed to switch workspace',
|
||
'workspace.switch_error_title': 'Workspace switch failed',
|
||
'workspace_setup.custom_protocol.rest': 'REST',
|
||
|
||
// Wizard
|
||
'wizard.back_catalog': 'Operations',
|
||
'wizard.progress.create': 'Create operation',
|
||
'wizard.progress.edit': 'Edit operation',
|
||
'wizard.exit': 'Exit wizard',
|
||
'wizard.step_of': 'Step <strong>{step}</strong> of {total}',
|
||
'wizard.step_short': 'Step {step}',
|
||
'wizard.status.completed': 'Completed',
|
||
'wizard.status.in_progress': 'In progress',
|
||
'wizard.status.not_started': 'Not started',
|
||
'wizard.help.title': 'Need help?',
|
||
'wizard.help.body': 'Unsure which protocol to choose? Read the',
|
||
'wizard.help.link': 'protocol comparison guide →',
|
||
'wizard.progress.section': 'Progress',
|
||
'wizard.step_name.protocol': 'Protocol',
|
||
'wizard.step_name.upstream': 'Upstream target',
|
||
'wizard.step_name.tool': 'Tool config',
|
||
'wizard.step_name.mapping': 'Mapping',
|
||
'wizard.button.back': 'Back',
|
||
'wizard.button.continue': 'Continue',
|
||
'wizard.button.save_draft': 'Save draft',
|
||
'wizard.button.create': 'Create operation',
|
||
'wizard.button.save_changes': 'Save changes',
|
||
'wizard.sidebar.brand.create': 'Create <span>operation</span>',
|
||
'wizard.sidebar.brand.edit': 'Edit <span>operation</span>',
|
||
'wizard.step1.title': 'Choose a protocol',
|
||
'wizard.step1.subtitle': 'Select the transport protocol your upstream service uses. This determines how Crank will communicate with your API and which configuration options are available in subsequent steps.',
|
||
'wizard.step1.rest_name': 'REST / HTTP',
|
||
'wizard.step1.rest_tagline': 'Standard HTTP verbs over a RESTful endpoint. The most broadly supported option.',
|
||
'wizard.step1.query': 'Query',
|
||
'wizard.step1.mutation': 'Mutation',
|
||
'wizard.step1.unary': 'Unary',
|
||
'wizard.step1.tip_title': 'You can change this later',
|
||
'wizard.step1.tip_body': 'Switching protocol after configuring subsequent steps will clear schema and mapping data. Save your operation as a draft before experimenting. See protocol migration guide for details.',
|
||
'wizard.step1.community_protocol_title': 'Community protocol scope',
|
||
'wizard.step5.streaming_title': 'Streaming execution in Community',
|
||
'wizard.step2.title': 'Select upstream & endpoint',
|
||
'wizard.step2.subtitle': 'Choose an existing upstream or register a new one. Then define the specific endpoint path this operation will call.',
|
||
'wizard.required': 'Required',
|
||
'wizard.optional': 'Optional',
|
||
'wizard.step2.upstream_title': 'API host',
|
||
'wizard.step2.upstream_subtitle': 'Base URL and authorization',
|
||
'wizard.step2.upstream_placeholder': 'Select an API host…',
|
||
'wizard.step2.search_placeholder': 'Search by name or URL…',
|
||
'wizard.step2.change': 'Change',
|
||
'wizard.step2.auth_none': 'No auth',
|
||
'wizard.step2.auth_bearer': 'Bearer token',
|
||
'wizard.step2.auth_basic': 'Basic auth',
|
||
'wizard.step2.auth_apikey': 'API key',
|
||
'wizard.step2.register_new': 'Add new API host',
|
||
'wizard.step2.name': 'Name',
|
||
'wizard.step2.base_url': 'Base URL',
|
||
'wizard.step2.unique_hint': 'Unique name for this API host.',
|
||
'wizard.step2.base_url_hint': 'Root URL without a trailing slash. Authorization is configured below.',
|
||
'wizard.step2.auth_selector': 'API host authorization',
|
||
'wizard.step2.auth_selector_hint': 'Use secrets so tokens and passwords are not stored directly in API host settings.',
|
||
'wizard.step2.auth_mode.none': 'No auth',
|
||
'wizard.step2.auth_mode.existing': 'Use existing authorization profile',
|
||
'wizard.step2.auth_mode.create': 'Create authorization profile now',
|
||
'wizard.step2.auth_profile': 'Authorization profile',
|
||
'wizard.step2.auth_profile_hint': 'The selected profile is applied automatically on every tool call.',
|
||
'wizard.step2.auth_profile_empty': 'No authorization profiles yet. Create one below.',
|
||
'wizard.step2.create_profile_title': 'Create authorization profile',
|
||
'wizard.step2.create_profile_subtitle': 'Create a reusable profile that reads values from encrypted workspace secrets.',
|
||
'wizard.step2.profile_name': 'Profile name',
|
||
'wizard.step2.profile_kind': 'Authorization type',
|
||
'wizard.step2.auth_kind.bearer': 'Bearer token',
|
||
'wizard.step2.auth_kind.basic': 'Basic auth',
|
||
'wizard.step2.auth_kind.api_key_header': 'API key in header',
|
||
'wizard.step2.auth_kind.api_key_query': 'API key in query parameter',
|
||
'wizard.step2.header_name': 'Header name',
|
||
'wizard.step2.query_param': 'Query parameter',
|
||
'wizard.step2.username_secret': 'Username secret',
|
||
'wizard.step2.password_secret': 'Password secret',
|
||
'wizard.step2.secret_value': 'Secret',
|
||
'wizard.step2.quick_secret': 'Quick create secret',
|
||
'wizard.step2.manage_secrets': 'Open secrets page',
|
||
'wizard.step2.static_headers': 'Static headers (optional)',
|
||
'wizard.step2.static_headers_hint': 'Optional headers without secret values. Store tokens, passwords and keys through an authorization profile.',
|
||
'wizard.step2.quick_secret_title': 'Quick create secret',
|
||
'wizard.step2.quick_secret_name': 'Secret name',
|
||
'wizard.step2.quick_secret_kind': 'Secret kind',
|
||
'wizard.step2.quick_secret_value': 'Secret value',
|
||
'wizard.step2.quick_secret_hint': 'The value is encrypted immediately and will not be shown again.',
|
||
'wizard.step2.quick_secret_submit': 'Create secret',
|
||
'wizard.step2.save_upstream': 'Save API host',
|
||
'wizard.step2.endpoint_title': 'Endpoint',
|
||
'wizard.step2.endpoint_subtitle': 'Path and HTTP method for this specific operation',
|
||
'wizard.step2.path_template': 'Path template',
|
||
'wizard.step2.path_hint': 'Appended to the API host Base URL. Use {param} for path parameters.',
|
||
'wizard.step3.rest.label': 'Request config',
|
||
'wizard.step3.rest.title': 'HTTP method & format',
|
||
'wizard.step3.rest.subtitle': 'Choose the HTTP method and request format. Crank converts MCP tool parameters into the API request body.',
|
||
'wizard.step3.rest.method_title': 'HTTP method',
|
||
'wizard.step3.rest.method_subtitle': 'Method sent to the API on every tool call',
|
||
'wizard.step3.rest.read': 'Read',
|
||
'wizard.step3.rest.create': 'Create',
|
||
'wizard.step3.rest.replace': 'Replace',
|
||
'wizard.step3.rest.update': 'Update',
|
||
'wizard.step3.rest.remove': 'Remove',
|
||
'wizard.step3.rest.format_title': 'Request format',
|
||
'wizard.step3.rest.format_subtitle': 'Request body format and expected response',
|
||
'wizard.step3.rest.content_type': 'Content-Type',
|
||
'wizard.step3.rest.content_type_hint': 'How the request body is encoded before sending it to the API.',
|
||
'wizard.step3.rest.accept': 'Accept',
|
||
'wizard.step3.rest.accept_hint': 'Response types accepted from the API server.',
|
||
'wizard.step4.title': 'Tool config',
|
||
'wizard.step4.subtitle': 'Name your tool, write the LLM-facing description, and define the input/output schemas. The description is the primary signal the model uses when deciding whether to call this tool.',
|
||
'wizard.step4.identity_title': 'Tool parameters',
|
||
'wizard.step4.identity_subtitle': 'Tool name and purpose description',
|
||
'wizard.step4.tool_name': 'Tool name',
|
||
'wizard.step4.tool_name_hint': 'Technical name for calling the MCP tool. Only lowercase English letters, numbers and underscores are allowed. Cannot be changed after publishing.',
|
||
'wizard.step4.display_name': 'Display name',
|
||
'wizard.step4.display_name_hint': 'Readable name shown in the interface and logs.',
|
||
'wizard.step4.tool_title': 'Short description',
|
||
'wizard.step4.tool_title_hint': 'Short phrase shown to the MCP client in the tool description.',
|
||
'wizard.step4.description': 'Description',
|
||
'wizard.step4.description_hint': 'Description for the model. Be precise: the model uses this text to decide when to call the tool.',
|
||
'wizard.step4.description_title': 'Writing effective descriptions',
|
||
'wizard.step4.description_body': 'Start with a verb: creates, fetches, updates. Mention the required input data and describe the successful result. Avoid vague wording like “handles” or “manages”.',
|
||
'wizard.step4.input_schema_title': 'Input schema',
|
||
'wizard.step4.input_schema_subtitle': 'Parameters the LLM passes when calling this tool',
|
||
'wizard.step4.output_schema_title': 'Output schema',
|
||
'wizard.step4.output_schema_subtitle': 'Shape of the data returned to the LLM after a successful call',
|
||
'wizard.step4.schema_draft': 'JSON Schema draft-07',
|
||
'wizard.step4.protocol_agnostic_title': 'Schemas describe the tool',
|
||
'wizard.step4.protocol_agnostic_body': 'The input schema describes what the MCP client sends to the tool. The output schema describes what the tool returns. On the next step, connect these fields to the real API request and response.',
|
||
'wizard.step5.title': 'Field bindings and launch',
|
||
'wizard.step5.subtitle': 'Connect MCP tool parameters to API request fields and connect the API response to the tool result. Authorization is configured on step 2.',
|
||
'wizard.step5.input_request': 'Tool input → API request',
|
||
'wizard.step5.output_response': 'API response → Tool output',
|
||
'wizard.step5.execution': 'Execution settings',
|
||
'wizard.step5.exec_title': 'Request execution',
|
||
'wizard.step5.exec_subtitle': 'Timeout, retry count and authorization profile',
|
||
'wizard.approval.title': 'Human confirmation',
|
||
'wizard.approval.subtitle': 'Enable this for actions that must not run without an explicit user decision.',
|
||
'wizard.approval.required_label': 'Require confirmation before execution',
|
||
'wizard.approval.required_desc': 'The MCP client receives a pending request, and the action runs only after confirmation through a separate approval endpoint.',
|
||
'wizard.approval.risk_level': 'Risk level',
|
||
'wizard.approval.risk.normal': 'Normal action',
|
||
'wizard.approval.risk.dangerous': 'Dangerous action',
|
||
'wizard.approval.risk.financial': 'Financial action',
|
||
'wizard.approval.risk.irreversible': 'Irreversible action',
|
||
'wizard.approval.ttl': 'How long to wait for confirmation',
|
||
'wizard.approval.confirmation_title': 'Confirmation title',
|
||
'wizard.approval.confirmation_title_hint': 'This text will be shown by the external confirmation interface.',
|
||
'wizard.approval.confirmation_body': 'Description for the user',
|
||
'wizard.approval.confirmation_body_hint': 'Explain briefly what will happen after confirmation.',
|
||
'wizard.approval.show_payload': 'Show request parameters',
|
||
'wizard.approval.payload_mode': 'How to show parameters',
|
||
'wizard.approval.payload.summary': 'Short summary',
|
||
'wizard.approval.payload.masked_json': 'JSON with masked secrets',
|
||
'wizard.approval.preview_label': 'Preview for confirmation UI',
|
||
'wizard.approval.default_title': 'Confirm operation execution',
|
||
'wizard.approval.default_body': 'Review operation parameters and confirm execution.',
|
||
'wizard.step5.security_level_title': 'Operation security',
|
||
'wizard.step5.community_security_note': '',
|
||
'wizard.step5.live_title': 'Check and publish',
|
||
'wizard.step5.draft_actions': 'Action result',
|
||
'wizard.step5.live_save_title': 'Saving before action',
|
||
'wizard.step5.live_save_body': 'Crank saves the current settings before running checks or publishing.',
|
||
'wizard.step5.agent_preview_title': 'How the MCP client sees this tool',
|
||
'wizard.step5.agent_preview_subtitle': 'Check the name, description, input schema and examples before publication.',
|
||
'wizard.step5.copy_agent_preview': 'Copy schema',
|
||
'wizard.step5.preview_tool_name': 'Tool name',
|
||
'wizard.step5.preview_tool_title': 'Short description',
|
||
'wizard.step5.preview_tool_description': 'Description for the model',
|
||
'wizard.step5.preview_input_schema': 'Input schema',
|
||
'wizard.step5.preview_tool_call': 'Example tools/call',
|
||
'wizard.step5.preview_success_response': 'Successful response example',
|
||
'wizard.step5.preview_error_response': 'Error example',
|
||
'wizard.step5.samples_title': 'Examples and automatic setup',
|
||
'wizard.step5.samples_subtitle': 'Save example input and output data, then let Crank rebuild schemas and field bindings from them.',
|
||
'wizard.step5.input_sample': 'Input example',
|
||
'wizard.step5.output_sample': 'Output example',
|
||
'wizard.step5.save_input_sample': 'Save input example',
|
||
'wizard.step5.save_output_sample': 'Save output example',
|
||
'wizard.step5.generate_draft': 'Rebuild from examples',
|
||
'wizard.step5.test_title': 'Operation testing',
|
||
'wizard.step5.test_subtitle': 'Run the tool against the real API before publishing it.',
|
||
'wizard.step5.test_input_payload': 'Input data for the test',
|
||
'wizard.step5.run_test': 'Run test',
|
||
'wizard.step5.use_response_output': 'Use response as output example',
|
||
'wizard.step5.request_preview': 'API request preview',
|
||
'wizard.step5.response_preview': 'API response preview',
|
||
'wizard.step5.errors': 'Errors',
|
||
'wizard.step5.choose_descriptor': 'Choose descriptor set',
|
||
'wizard.step5.upload_discover': 'Upload and discover services',
|
||
'wizard.step5.no_descriptor': 'No descriptor set selected',
|
||
'wizard.step5.yaml_title': 'YAML import and export',
|
||
'wizard.step5.yaml_subtitle': 'Export the operation to a file or import a prepared YAML configuration.',
|
||
'wizard.step5.export_yaml': 'Export YAML',
|
||
'wizard.step5.load_yaml_file': 'Load YAML file',
|
||
'wizard.step5.import_yaml': 'Import YAML',
|
||
'wizard.step5.yaml_document': 'YAML configuration',
|
||
'wizard.step5.yaml_placeholder': 'Paste YAML here to create or update an operation.',
|
||
'wizard.step5.publish_title': 'Publication',
|
||
'wizard.step5.publish_subtitle': 'Publish the operation so it can be added to agents.',
|
||
'wizard.step5.publish_hint': 'Before publication, Crank saves the current settings from this wizard.',
|
||
'wizard.step5.publish_action': 'Publish operation',
|
||
'wizard.step5.editable_title': 'Operation stays editable in this wizard',
|
||
'wizard.step5.editable_body': 'Saving updates the operation settings in this wizard.',
|
||
'wizard.live.failed_title': 'Action failed',
|
||
'wizard.live.failed_body': 'The request failed. Check the operation settings and try again.',
|
||
'wizard.error.no_workspace': 'No workspace selected',
|
||
'wizard.error.tool_name': 'Tool name is required',
|
||
'wizard.error.parser_unavailable': 'YAML parser is not available',
|
||
'wizard.error.select_upstream': 'Select or add an API host first',
|
||
'wizard.error.auth_profile_required': 'Select an authorization profile or switch API host authorization to No auth.',
|
||
'wizard.error.auth_profile_name': 'Authorization profile name is required.',
|
||
'wizard.error.secret_required': 'Select the secret required by this authorization profile.',
|
||
'wizard.error.basic_secrets_required': 'Select both username and password secrets.',
|
||
'wizard.error.header_name_required': 'Header name is required for this authorization profile.',
|
||
'wizard.error.query_param_required': 'Query parameter name is required for this authorization profile.',
|
||
'wizard.error.secret_name_required': 'Secret name is required.',
|
||
'wizard.error.secret_value_required': 'Secret value is required.',
|
||
'wizard.error.save_upstream_first': 'Save the API host after creating a new authorization profile before continuing.',
|
||
'wizard.toast.auth_profile_created_title': 'Authorization profile created',
|
||
'wizard.toast.auth_profile_created_body': 'New authorization profile is ready and selected.',
|
||
'wizard.toast.auth_profile_error_title': 'Authorization profile creation failed',
|
||
'wizard.toast.quick_secret_created_title': 'Secret created',
|
||
'wizard.toast.quick_secret_created_body': 'The new secret is now available in the authorization selector.',
|
||
'wizard.toast.quick_secret_error_title': 'Secret creation failed',
|
||
'wizard.error.save': 'Failed to save operation',
|
||
'wizard.error.save_title': 'Save failed',
|
||
'wizard.save.created': 'Draft created',
|
||
'wizard.save.updated': 'Draft updated',
|
||
'wizard.save.body': 'The current operation settings have been saved.',
|
||
'wizard.save.saved': 'Saved ✓',
|
||
'wizard.busy.input_sample': 'Saving input example…',
|
||
'wizard.busy.output_sample': 'Saving output example…',
|
||
'wizard.busy.generate': 'Rebuilding settings…',
|
||
'wizard.busy.test': 'Running test…',
|
||
'wizard.busy.quality': 'Checking quality…',
|
||
'wizard.busy.export_yaml': 'Exporting YAML…',
|
||
'wizard.busy.import_yaml': 'Importing YAML…',
|
||
'wizard.busy.publish': 'Publishing operation…',
|
||
'wizard.busy.descriptor': 'Uploading descriptor set…',
|
||
'wizard.busy.wsdl': 'Inspecting WSDL…',
|
||
'wizard.sample.input_saved': 'Input example saved',
|
||
'wizard.sample.input_saved_body': 'The input example has been saved for this operation.',
|
||
'wizard.sample.output_saved': 'Output example saved',
|
||
'wizard.sample.output_saved_body': 'The output example has been saved for this operation.',
|
||
'wizard.sample.generated': 'Settings rebuilt',
|
||
'wizard.sample.generated_body': 'Schemas and field bindings were rebuilt from the saved examples.',
|
||
'wizard.test.completed': 'Operation test completed',
|
||
'wizard.test.failed': 'Operation test returned errors',
|
||
'wizard.test.completed_body': 'The operation was executed successfully against the API.',
|
||
'wizard.test.failed_body': 'The operation was executed, but the API or validation returned errors.',
|
||
'wizard.test.window_completed': 'Window test completed',
|
||
'wizard.test.window_completed_body': 'Collected a bounded result window.',
|
||
'wizard.test.window_truncated_note': 'Results were truncated.',
|
||
'wizard.test.window_more_note': 'More data is available if you continue from the streaming views.',
|
||
'wizard.test.window_incomplete_note': 'The upstream stream ended before the configured window was fully collected.',
|
||
'wizard.test.no_response': 'No test response yet',
|
||
'wizard.test.no_response_body': 'Run a test first, then copy the response into the output example.',
|
||
'wizard.test.response_copied': 'Response copied',
|
||
'wizard.test.response_copied_body': 'The latest test response was copied into the output example.',
|
||
'wizard.agent_preview.copied': 'Schema copied',
|
||
'wizard.agent_preview.copied_body': 'The MCP tool schema was copied to the clipboard.',
|
||
'wizard.quality.title': 'Tool quality check',
|
||
'wizard.quality.subtitle': 'Check the name, description, schemas and the result visible to the agent.',
|
||
'wizard.quality.action': 'Check quality',
|
||
'wizard.quality.empty': 'Quality check has not run yet.',
|
||
'wizard.quality.no_findings': 'No issues found.',
|
||
'wizard.quality.checked_title': 'Quality check completed',
|
||
'wizard.quality.checked_body': 'Review the recommendations before publishing.',
|
||
'wizard.quality.blocked_title': 'Quality check found blocking issues',
|
||
'wizard.quality.blocked_body': 'Fix error-level findings before publishing.',
|
||
'wizard.quality.blocking_error': 'Fix blocking quality findings before publishing.',
|
||
'wizard.quality.severity_error': 'Error',
|
||
'wizard.quality.severity_warning': 'Warning',
|
||
'wizard.quality.severity_info': 'Info',
|
||
'wizard.boolean.yes': 'yes',
|
||
'wizard.boolean.no': 'no',
|
||
'wizard.yaml.exported': 'YAML exported',
|
||
'wizard.yaml.exported_body': 'The current operation configuration was downloaded as YAML.',
|
||
'wizard.yaml.none': 'No YAML provided',
|
||
'wizard.yaml.none_body': 'Paste a YAML document or load it from a file before importing.',
|
||
'wizard.yaml.imported': 'YAML imported',
|
||
'wizard.yaml.imported_with_warnings': 'YAML imported with recommendations',
|
||
'wizard.yaml.imported_body': 'The imported operation was saved. The wizard will reopen it now.',
|
||
'wizard.yaml.loaded': 'YAML loaded',
|
||
'wizard.yaml.loaded_body': 'The YAML configuration was loaded.',
|
||
'wizard.publish.done': 'Operation published',
|
||
'wizard.publish.done_body': 'Version {version} is now published and can be bound into agents.',
|
||
|
||
// Common buttons
|
||
'btn.save': 'Save changes',
|
||
'btn.cancel': 'Cancel',
|
||
'btn.create': 'Create',
|
||
'btn.delete': 'Delete',
|
||
'btn.revoke': 'Revoke',
|
||
'btn.copy': 'Copy',
|
||
|
||
// Agents page
|
||
'agents.title': 'Agents',
|
||
'agents.subtitle': 'Group your MCP tools by AI agent.',
|
||
'agents.subtitle.community': 'Group your MCP tools by AI agent.',
|
||
'agents.new': 'New agent',
|
||
'agents.limit.title': 'Agent limit reached',
|
||
'agents.limit.message': 'This edition allows up to {limit} AI agent. Remove an existing agent to add another one.',
|
||
'agents.stats.total': 'Total agents',
|
||
'agents.stats.total_sub': 'across this workspace',
|
||
'agents.stats.published': 'Published',
|
||
'agents.stats.published_sub': '{value}% of total',
|
||
'agents.stats.tools': 'Operations exposed',
|
||
'agents.stats.tools_sub': 'across {count} agents',
|
||
'agents.stats.calls': 'Calls today',
|
||
'agents.stats.calls_sub': 'all agents combined',
|
||
'agents.search': 'Search agents...',
|
||
'agents.results': '{shown} of {total} agents',
|
||
'agents.callout.default': 'Group MCP tools around concrete tasks for a concrete agent. Do not give one LLM too many tools at once: it can increase mistakes and hallucinations. Each agent has its own API keys.',
|
||
'agents.callout.community': 'Group MCP tools around concrete tasks for a concrete agent. Do not give one LLM too many tools at once: it can increase mistakes and hallucinations. Each agent has its own API keys.',
|
||
'agents.loading': 'Loading agents…',
|
||
'agents.error.title': 'Failed to load agents',
|
||
'agents.error.api': 'Workspace or API is unavailable',
|
||
'agents.error.load': 'Failed to load agents',
|
||
'agents.empty.initial.title': 'No agents yet',
|
||
'agents.empty.initial.sub': 'Create your first agent to get a dedicated MCP endpoint with a curated set of tools.',
|
||
'agents.empty.initial.sub_community': 'Create your first agent to get a dedicated MCP endpoint with a focused toolset.',
|
||
'agents.empty.initial.next_step': 'After creating an agent, issue an API key for it on the API Keys page.',
|
||
'agents.empty.search.title': 'No agents match "{query}"',
|
||
'agents.empty.search.sub': 'Try a different search term.',
|
||
'agents.card.tools': 'tools',
|
||
'agents.card.keys': 'keys',
|
||
'agents.card.calls_today': 'calls today',
|
||
'agents.card.created': 'Created {date}',
|
||
'agents.card.copy_endpoint': 'Copy endpoint',
|
||
'agents.card.endpoint_help': 'Set this endpoint in the MCP client together with the API key.',
|
||
'agents.card.endpoint_help_community': 'Set this endpoint in the MCP client together with the API key.',
|
||
'agents.action.edit': 'Edit',
|
||
'agents.action.archive': 'Archive',
|
||
'agents.action.delete': 'Delete',
|
||
'agents.lifecycle.publish': 'Publish',
|
||
'agents.lifecycle.unpublish': 'Unpublish',
|
||
'agents.lifecycle.restore_draft': 'Restore draft',
|
||
'agents.lifecycle.published': 'Published',
|
||
'agents.lifecycle.archived': 'Archived',
|
||
'agents.lifecycle.draft': 'Draft',
|
||
'agents.drawer.new_title': 'New agent',
|
||
'agents.drawer.edit_title': 'Edit agent',
|
||
'agents.drawer.new_sub': 'Describe the agent and select the MCP tools available to it.',
|
||
'agents.drawer.new_sub_community': 'Describe the agent and select the MCP tools available to it.',
|
||
'agents.drawer.edit_sub': 'Update agent settings and tool selection',
|
||
'agents.drawer.identity': 'Identity',
|
||
'agents.drawer.display_name': 'Display name',
|
||
'agents.drawer.slug': 'Slug',
|
||
'agents.drawer.description': 'Description',
|
||
'agents.drawer.optional': '(optional)',
|
||
'agents.drawer.required': 'required',
|
||
'agents.drawer.status': 'Status',
|
||
'agents.drawer.endpoint': 'MCP endpoint',
|
||
'agents.drawer.slug_hint': 'Slug is used as part of the endpoint to identify the agent.',
|
||
'agents.drawer.placeholder.name': 'Customer Support',
|
||
'agents.drawer.placeholder.slug': 'customer-support',
|
||
'agents.drawer.placeholder.description': 'What does this agent do? What scenario is it for?',
|
||
'agents.drawer.operations': 'Operations',
|
||
'agents.drawer.selected': '{count} selected',
|
||
'agents.drawer.operations_sub': 'Select the MCP tools available to this agent.',
|
||
'agents.drawer.operations_sub_community': 'Select the MCP tools available to this agent.',
|
||
'agents.drawer.finding.title': 'Recommendation',
|
||
'agents.drawer.finding.too_many_tools': 'This agent has many tools. Keep only the tools needed for one concrete task.',
|
||
'agents.drawer.finding.similar_tools': 'Tools “{left}” and “{right}” look similar. Rename them more precisely or keep one of them.',
|
||
'agents.drawer.filter_ops': 'Filter operations…',
|
||
'agents.drawer.ops_no_match': 'No operations match "{query}"',
|
||
'agents.drawer.ops_selected': '{count} operations selected',
|
||
'agents.drawer.clear_all': 'Clear all',
|
||
'agents.drawer.recommendation': "You've selected {count} tools. LLMs usually work best when an agent has fewer than 15 tools. Consider splitting this into separate agents by use case.",
|
||
'agents.drawer.cancel': 'Cancel',
|
||
'agents.drawer.create': 'Create agent',
|
||
'agents.drawer.save': 'Save changes',
|
||
'agents.toast.saved_title_create': 'Agent created',
|
||
'agents.toast.saved_title_update': 'Agent updated',
|
||
'agents.toast.saved_message': '{name} was saved with {count} tool bindings.',
|
||
'agents.toast.save_error_title': 'Agent update failed',
|
||
'agents.toast.save_error_message': 'Failed to save agent',
|
||
'agents.toast.delete_confirm': 'Delete this agent? This cannot be undone.',
|
||
'agents.toast.delete_title': 'Agent deleted',
|
||
'agents.toast.delete_message': '{name} was deleted.',
|
||
'agents.toast.delete_error_title': 'Delete failed',
|
||
'agents.toast.delete_error_message': 'Failed to delete agent',
|
||
'agents.toast.lifecycle_title': 'Agent lifecycle updated',
|
||
'agents.toast.lifecycle_publish': '{name} was published.',
|
||
'agents.toast.lifecycle_unpublish': '{name} was returned to draft.',
|
||
'agents.toast.lifecycle_archive': '{name} was archived.',
|
||
'agents.toast.lifecycle_error_title': 'Lifecycle update failed',
|
||
'agents.toast.lifecycle_error_message': 'Failed to update agent lifecycle',
|
||
'agents.toast.endpoint_title': 'MCP endpoint copied',
|
||
|
||
// Demo content
|
||
'demo.agent.currency-rates.display_name': 'Currency rates',
|
||
'demo.operation.frankfurter_latest_rate.display_name': 'Latest currency rate',
|
||
'demo.operation.frankfurter_latest_rate.description': 'Returns the latest available exchange rate through Frankfurter.',
|
||
|
||
// Login
|
||
'login.title': 'Sign in',
|
||
'login.subtitle': 'Welcome back to your workspace',
|
||
'login.email': 'Email',
|
||
'login.password': 'Password',
|
||
'login.submit': 'Sign in',
|
||
'login.email_label': 'Email address',
|
||
'login.email_placeholder': 'you@acme.com',
|
||
'login.password_only': 'Sign in with email and password.',
|
||
'login.error.required': 'Please enter your email and password.',
|
||
'login.error.invalid': 'Invalid email or password. Please try again.',
|
||
'login.error.generic': 'Unable to sign in right now. Please try again.',
|
||
},
|
||
|
||
ru: {
|
||
// Nav
|
||
'nav.operations': 'Операции',
|
||
'nav.agents': 'Агенты',
|
||
'nav.apikeys': 'API Ключи',
|
||
'nav.secrets': 'Секреты',
|
||
'nav.logs': 'Логи',
|
||
'nav.usage': 'Использование',
|
||
'nav.settings': 'Настройки',
|
||
'nav.logout': 'Выйти',
|
||
'nav.workspaces': 'Ваши воркспейсы',
|
||
'nav.menu': 'Меню',
|
||
'nav.account': 'Аккаунт',
|
||
'nav.user_fallback': 'Crank',
|
||
'nav.workspace_fallback': 'workspace',
|
||
|
||
// Operations page
|
||
'ops.title': 'Операции',
|
||
'ops.subtitle': 'Каталог инструментов. Список созданных MCP инструментов на API эндпоинты.',
|
||
'ops.new': 'Новая операция',
|
||
'ops.loading': 'Загрузка операций…',
|
||
'ops.empty.title': 'Операции не найдены',
|
||
'ops.empty.sub': 'Попробуйте изменить поисковый запрос или фильтры.',
|
||
'ops.empty.initial.title': 'Операций пока нет',
|
||
'ops.empty.initial.sub': 'Создайте первую операцию в этом воркспейсе, чтобы открыть живой MCP-инструмент.',
|
||
'ops.error.title': 'Не удалось подключиться к серверу',
|
||
'ops.delete.confirm': 'Удалить эту операцию? Действие нельзя отменить.',
|
||
'ops.delete.success.title': 'Операция удалена',
|
||
'ops.delete.success.message': 'Операция {name} удалена.',
|
||
'ops.delete.error.title': 'Не удалось удалить',
|
||
'ops.delete.error.message': 'Не удалось удалить операцию',
|
||
'ops.action.edit': 'Редактировать операцию',
|
||
'ops.action.delete':'Удалить операцию',
|
||
'ops.stats.synced': 'Каталог синхронизирован с сервером',
|
||
'ops.stats.none': 'Операций пока нет',
|
||
'ops.stats.share': '{value}% от общего числа',
|
||
'ops.stats.no_published': 'Опубликованных инструментов пока нет',
|
||
'ops.stats.requests_rollup': 'Сумма по статистике операций',
|
||
'ops.stats.no_traffic': 'Трафик сегодня не зафиксирован',
|
||
'ops.stats.latency_weighted': 'Взвешено по сегодняшним вызовам',
|
||
'ops.stats.latency_waiting': 'Ожидание runtime-метрик',
|
||
'ops.results': 'Показано {shown} из {total}',
|
||
'ops.range': 'Показано {from}–{to} из {total} операций',
|
||
'ops.filter.category': 'Категория: {value}',
|
||
'ops.filter.agent': 'Агент: {value}',
|
||
'ops.no_active_agents': 'Нет активных агентов',
|
||
|
||
// Stats
|
||
'stats.total': 'Всего операций',
|
||
'stats.active': 'Активные',
|
||
'stats.requests': 'Запросов сегодня',
|
||
'stats.latency': 'Ср. задержка',
|
||
|
||
// Tabs
|
||
'tab.all': 'Все',
|
||
'tab.active': 'Активные',
|
||
'tab.draft': 'Черновик',
|
||
'tab.error': 'Ошибка',
|
||
'tab.inactive': 'Неактивные',
|
||
|
||
// Filters & sort
|
||
'filter.search': 'Поиск операций...',
|
||
'filter.status': 'Статус',
|
||
'filter.category': 'Категория',
|
||
'filter.agent': 'Агент',
|
||
'filter.showing': 'Показано',
|
||
'filter.of': 'из',
|
||
'filter.clear': 'Сбросить',
|
||
'filter.clear_all': 'Сбросить все',
|
||
'filter.remove': 'Убрать фильтр',
|
||
'sort.created_desc':'Сначала новые',
|
||
'sort.created_asc': 'Сначала старые',
|
||
'sort.name_asc': 'Название А–Я',
|
||
'sort.name_desc': 'Название Я–А',
|
||
|
||
// Table headers
|
||
'th.name': 'НАЗВАНИЕ',
|
||
'th.protocol': 'ПРОТОКОЛ',
|
||
'th.status': 'СТАТУС',
|
||
'th.created': 'СОЗДАН',
|
||
'th.url': 'ЦЕЛЕВОЙ URL',
|
||
|
||
// Pagination
|
||
'page.showing': 'Показано',
|
||
'page.of': 'из',
|
||
'page.ops': 'операций',
|
||
|
||
// API Keys page
|
||
'apikeys.title': 'Ключи агентов',
|
||
'apikeys.subtitle': 'Эти ключи используются для подключения MCP клиента к MCP серверу и выдаются на конкретного агента.',
|
||
'apikeys.new': 'Создать ключ',
|
||
'apikeys.new_mcp': 'Создать ключ MCP-клиента',
|
||
'apikeys.new_approval': 'Создать ключ подтверждения',
|
||
'apikeys.kind.mcp': 'MCP-клиенты',
|
||
'apikeys.kind.approval': 'Подтверждения',
|
||
'apikeys.kind.mcp_hint': 'Ключи MCP-клиентов используются для подключения к tools/list и tools/call агента.',
|
||
'apikeys.kind.approval_hint': 'Ключи подтверждения используются только внешним интерфейсом, где человек подтверждает действие.',
|
||
'apikeys.agent.title': 'Выбор агента',
|
||
'apikeys.agent.subtitle': 'Выберите AI-агента для которого выпускается ключ.',
|
||
'apikeys.agent.label': 'AI-агент',
|
||
'apikeys.agent.empty': 'Агенты отсутствуют',
|
||
'apikeys.agent.empty_hint': 'Сначала создайте AI-агента, затем выпустите машинный ключ для его MCP endpoint.',
|
||
'apikeys.agent.endpoint': 'MCP endpoint: {endpoint}',
|
||
'apikeys.agent.endpoint_missing': 'У выбранного агента пока нет опубликованного MCP endpoint.',
|
||
'apikeys.th.name': 'НАЗВАНИЕ',
|
||
'apikeys.th.scope': 'ДОСТУП',
|
||
'apikeys.th.created':'СОЗДАН',
|
||
'apikeys.th.last': 'ПОСЛЕДНЕЕ ИСПОЛЬЗОВАНИЕ',
|
||
'apikeys.callout.title': 'Ключ показывается только один раз.',
|
||
'apikeys.callout.body': 'Скопируйте и сохраните ключ сразу после создания — Crank хранит только хэш. Поле «Последнее использование» обновляется после успешного MCP-вызова.',
|
||
'apikeys.active.title': 'Активные ключи',
|
||
'apikeys.active.subtitle': '{active} активных · {revoked} отозванных',
|
||
'apikeys.search': 'Фильтр ключей…',
|
||
'apikeys.th.prefix': 'ПРЕФИКС КЛЮЧА',
|
||
'apikeys.th.scopes': 'ДОСТУПЫ',
|
||
'apikeys.th.status': 'СТАТУС',
|
||
'apikeys.scope_ref': 'Справка по доступам',
|
||
'apikeys.scope.read': 'Инициализация MCP-сессий, ping сервера и получение списка инструментов для выбранного AI-агента.',
|
||
'apikeys.scope.write': 'Выполнение `tools/call` для опубликованных наборов инструментов агента.',
|
||
'apikeys.scope.deploy': 'Зарезервировано для deploy-автоматизации. Сейчас также разрешает MCP read/write сценарии.',
|
||
'apikeys.scope.approve': 'Подтверждение ожидающего human approval запроса для этого агента.',
|
||
'apikeys.scope.deny': 'Отклонение ожидающего human approval запроса для этого агента.',
|
||
'apikeys.modal.title': 'Создать ключ агента',
|
||
'apikeys.modal.title_mcp': 'Создать ключ MCP-клиента',
|
||
'apikeys.modal.title_approval': 'Создать ключ подтверждения',
|
||
'apikeys.modal.name': 'Имя ключа',
|
||
'apikeys.modal.name_hint': 'Понятная метка для идентификации ключа. Видна только администраторам.',
|
||
'apikeys.modal.name_placeholder': 'например, Production, CI pipeline',
|
||
'apikeys.modal.scopes': 'Доступы',
|
||
'apikeys.modal.reveal_title': 'Скопируйте ключ сейчас.',
|
||
'apikeys.modal.reveal_body': 'Повторно он не будет показан.',
|
||
'apikeys.modal.copy': 'Скопировать',
|
||
'apikeys.modal.cancel': 'Отмена',
|
||
'apikeys.modal.create': 'Создать ключ',
|
||
'apikeys.modal.done': 'Готово — ключ скопирован',
|
||
'apikeys.status.active': 'Активен',
|
||
'apikeys.status.revoked': 'Отозван',
|
||
'apikeys.last_used.never': 'Никогда',
|
||
'apikeys.empty.none': 'API-ключей пока нет',
|
||
'apikeys.empty.approval': 'Ключей подтверждения пока нет. Они нужны только агентам с инструментами, требующими подтверждения человеком.',
|
||
'apikeys.empty.search': 'Нет ключей по текущему поиску',
|
||
'apikeys.loading': 'Загрузка…',
|
||
'apikeys.error.api': 'Воркспейс или API недоступен',
|
||
'apikeys.error.load': 'Не удалось загрузить API-ключи',
|
||
'apikeys.confirm.revoke': 'Отозвать этот ключ? Все запросы с ним сразу начнут падать.',
|
||
'apikeys.confirm.delete': 'Удалить этот ключ? Действие нельзя отменить.',
|
||
'apikeys.toast.revoke_title': 'Ключ отозван',
|
||
'apikeys.toast.revoke_message': 'Ключ {name} отозван.',
|
||
'apikeys.toast.revoke_error_title': 'Не удалось отозвать',
|
||
'apikeys.toast.revoke_error_message': 'Не удалось отозвать ключ',
|
||
'apikeys.toast.delete_title': 'Ключ удален',
|
||
'apikeys.toast.delete_message': 'Ключ {name} удален.',
|
||
'apikeys.toast.delete_error_title': 'Не удалось удалить',
|
||
'apikeys.toast.delete_error_message': 'Не удалось удалить ключ',
|
||
'apikeys.toast.scope_missing_title': 'Не выбран доступ',
|
||
'apikeys.toast.scope_missing_message': 'Выберите хотя бы один доступ перед созданием ключа.',
|
||
'apikeys.toast.create_title': 'Ключ агента создан',
|
||
'apikeys.toast.create_message': 'Скопируйте ключ сейчас. Повторно он не будет показан.',
|
||
'apikeys.toast.create_error_title': 'Не удалось создать ключ',
|
||
'apikeys.toast.create_error_message': 'Не удалось создать ключ',
|
||
'apikeys.toast.copy_title': 'Ключ агента скопирован',
|
||
'apikeys.toast.copy_message': 'Сохраните исходный ключ в надежном месте. Повторно показать его нельзя.',
|
||
'apikeys.toast.prefix_title': 'Префикс ключа скопирован',
|
||
'apikeys.action.copy_prefix': 'Скопировать префикс ключа',
|
||
'apikeys.action.revoke': 'Отозвать ключ',
|
||
'apikeys.action.delete': 'Удалить',
|
||
'apikeys.creating': 'Создание…',
|
||
'apikeys.approval.warning': 'Не передавайте этот ключ LLM или MCP-клиенту. Он нужен только внешнему интерфейсу, где человек подтверждает действие.',
|
||
|
||
// Secrets page
|
||
'secrets.title': 'Секреты',
|
||
'secrets.subtitle': 'Данные для авторизации на конечных API хостах.',
|
||
'secrets.new': 'Создать секрет',
|
||
'secrets.callout.title': 'Значения секретов защищены.',
|
||
'secrets.callout.body': 'После сохранения секреты шифруются ключом воркспейса и недоступны для чтения. Секрет можно ротировать или удалить.',
|
||
'secrets.active.title': 'Секреты воркспейса',
|
||
'secrets.active.subtitle': '{active} активных · {total} всего',
|
||
'secrets.search': 'Фильтр секретов…',
|
||
'secrets.th.name': 'НАЗВАНИЕ',
|
||
'secrets.th.kind': 'ТИП',
|
||
'secrets.th.version': 'ВЕРСИЯ',
|
||
'secrets.th.last_used': 'ПОСЛЕДНЕЕ ИСПОЛЬЗОВАНИЕ',
|
||
'secrets.th.used_by': 'ИСПОЛЬЗУЕТСЯ В',
|
||
'secrets.th.status': 'СТАТУС',
|
||
'secrets.kind.token': 'Токен',
|
||
'secrets.kind.username_password': 'Логин/пароль',
|
||
'secrets.kind.header': 'Значение заголовка',
|
||
'secrets.kind.generic': 'Общий JSON',
|
||
'secrets.status.active': 'Активен',
|
||
'secrets.status.disabled': 'Отключен',
|
||
'secrets.last_used.never': 'Никогда',
|
||
'secrets.used_by_count.one': '{count} привязка',
|
||
'secrets.used_by_count.few': '{count} привязки',
|
||
'secrets.used_by_count.many': '{count} привязок',
|
||
'secrets.used_by_count.other': '{count} привязки',
|
||
'secrets.loading': 'Загрузка…',
|
||
'secrets.empty.none': 'Секретов пока нет',
|
||
'secrets.empty.search': 'Нет секретов по текущему поиску',
|
||
'secrets.error.workspace': 'Воркспейс не выбран',
|
||
'secrets.error.load': 'Не удалось загрузить секреты',
|
||
'secrets.action.rotate': 'Ротировать',
|
||
'secrets.action.delete': 'Удалить',
|
||
'secrets.confirm.delete': 'Удалить секрет {name}? Это нельзя сделать, пока секрет используется в операции.',
|
||
'secrets.toast.create_title': 'Секрет создан',
|
||
'secrets.toast.create_message': 'Секрет сохранен в зашифрованном виде. Повторно посмотреть значение нельзя.',
|
||
'secrets.toast.create_error_title': 'Не удалось создать секрет',
|
||
'secrets.toast.create_error_message': 'Не удалось создать секрет',
|
||
'secrets.toast.rotate_title': 'Секрет ротирован',
|
||
'secrets.toast.rotate_message': 'Новая версия секрета теперь активна.',
|
||
'secrets.toast.rotate_error_title': 'Не удалось ротировать секрет',
|
||
'secrets.toast.rotate_error_message': 'Не удалось ротировать секрет',
|
||
'secrets.toast.delete_title': 'Секрет удален',
|
||
'secrets.toast.delete_message': 'Секрет {name} удален.',
|
||
'secrets.toast.delete_error_title': 'Не удалось удалить секрет',
|
||
'secrets.toast.delete_error_message': 'Не удалось удалить секрет',
|
||
'secrets.modal.create_title': 'Создать секрет',
|
||
'secrets.modal.rotate_title': 'Ротировать секрет',
|
||
'secrets.modal.name': 'Имя секрета',
|
||
'secrets.modal.name_hint': 'Укажите понятное имя, чтобы потом выбрать этот секрет в настройках операции.',
|
||
'secrets.modal.kind': 'Тип секрета',
|
||
'secrets.modal.value': 'Значение токена',
|
||
'secrets.modal.header_value': 'Значение заголовка',
|
||
'secrets.modal.username': 'Логин',
|
||
'secrets.modal.password': 'Пароль',
|
||
'secrets.modal.json_payload': 'JSON',
|
||
'secrets.modal.json_hint': 'Введите валидный JSON. Используйте этот тип, если API требует несколько связанных значений.',
|
||
'secrets.modal.create_note': 'После сохранения значение будет зашифровано и больше не будет отображаться.',
|
||
'secrets.modal.rotate_note': 'Ротация заменит сохраненное значение новой зашифрованной версией.',
|
||
'secrets.modal.cancel': 'Отмена',
|
||
'secrets.modal.create_action': 'Создать секрет',
|
||
'secrets.modal.rotate_action': 'Ротировать секрет',
|
||
'secrets.modal.creating': 'Создание…',
|
||
'secrets.modal.rotating': 'Ротация…',
|
||
'secrets.hint.token': 'Для bearer token, API key или другого одиночного токена.',
|
||
'secrets.hint.username_password': 'Для авторизации по логину и паролю.',
|
||
'secrets.hint.header': 'Для значения, которое будет передаваться в HTTP-заголовке.',
|
||
'secrets.hint.generic': 'Для структурированных данных в JSON.',
|
||
'secrets.validation.name_required': 'Нужно указать имя секрета.',
|
||
'secrets.validation.value_required': 'Нужно указать значение секрета.',
|
||
'secrets.validation.username_password_required': 'Нужно указать и логин, и пароль.',
|
||
'secrets.validation.json_invalid': 'Значение JSON должно быть валидным.',
|
||
// Logs page
|
||
'logs.title': 'Логи',
|
||
'logs.subtitle': 'Журнал вызовов по всем операциям этого воркспейса.',
|
||
'logs.search': 'Поиск в логах...',
|
||
'logs.level.all': 'Все',
|
||
'logs.level.info': 'Info',
|
||
'logs.level.warn': 'Warn',
|
||
'logs.level.error': 'Error',
|
||
'logs.level.debug': 'Debug',
|
||
'logs.live': 'Live',
|
||
'logs.paused': 'Пауза',
|
||
'logs.refresh': 'Обновить',
|
||
'logs.range.30m': 'Последние 30 мин',
|
||
'logs.range.1h': 'Последний час',
|
||
'logs.range.6h': 'Последние 6 часов',
|
||
'logs.range.24h': 'Последние 24 часа',
|
||
'logs.range.7d': 'Последние 7 дней',
|
||
'logs.search_messages': 'Поиск по сообщениям…',
|
||
'logs.detail.agent': 'Агент',
|
||
'logs.detail.request': 'Предпросмотр запроса',
|
||
'logs.detail.response': 'Предпросмотр ответа',
|
||
'logs.detail.meta': 'Метаданные',
|
||
'logs.loading.title': 'Загрузка логов…',
|
||
'logs.loading.sub': 'Получаем историю вызовов для текущего воркспейса.',
|
||
'logs.error.title': 'Не удалось загрузить логи',
|
||
'logs.error.api': 'Admin API недоступен',
|
||
'logs.error.workspace': 'Воркспейс не выбран',
|
||
'logs.error.load': 'Не удалось загрузить логи',
|
||
'logs.empty.title': 'Записей логов не найдено',
|
||
'logs.empty.filtered': 'Попробуйте расширить период или сбросить текущий поиск и фильтры уровня.',
|
||
'logs.empty.initial': 'Запустите тесты или опубликованные MCP-инструменты, чтобы начать собирать историю вызовов.',
|
||
'logs.live.on.title': 'Live режим включен',
|
||
'logs.live.on.body': 'Логи опрашиваются каждые 4 секунды.',
|
||
'logs.live.off.title': 'Live режим на паузе',
|
||
'logs.live.off.body': 'Автоматический опрос остановлен.',
|
||
'logs.refresh.title': 'Логи обновлены',
|
||
'logs.refresh.body': 'Получены последние записи вызовов для текущего воркспейса.',
|
||
|
||
// Usage page
|
||
'usage.title': 'Использование',
|
||
'usage.subtitle': 'Метрики вызовов по всем операциям этого воркспейса.',
|
||
'usage.export': 'Экспорт CSV',
|
||
'usage.period.7d': 'Последние 7 дней',
|
||
'usage.period.30d': 'Последние 30 дней',
|
||
'usage.period.90d': 'Последние 90 дней',
|
||
'usage.period.this_month': 'Этот месяц',
|
||
'usage.period.label.7d': 'последние 7 дней',
|
||
'usage.period.label.30d': 'последние 30 дней',
|
||
'usage.period.label.90d': 'последние 90 дней',
|
||
'usage.period.label.this_month': 'этот месяц',
|
||
'usage.stats.total': 'Всего вызовов',
|
||
'usage.stats.success': 'Доля успешных',
|
||
'usage.stats.p50': 'Медианная задержка (p50)',
|
||
'usage.stats.p99': 'Задержка p99',
|
||
'usage.stats.across': 'За период: {period}',
|
||
'usage.stats.success_calls': 'Успешных вызовов: {count}',
|
||
'usage.stats.median': 'Медианная задержка за выбранный период',
|
||
'usage.stats.error_calls': 'Ошибочных вызовов: {count}',
|
||
'usage.chart.title': 'Вызовы во времени',
|
||
'usage.chart.success': 'Успех',
|
||
'usage.chart.error': 'Ошибка',
|
||
'usage.chart.empty.title': 'Данных по использованию пока нет',
|
||
'usage.chart.empty.sub': 'Метрики появятся здесь после тестов или вызовов опубликованных инструментов за выбранный период.',
|
||
'usage.table.title': 'По операциям',
|
||
'usage.table.subtitle': 'Разбивка за период: {period}',
|
||
'usage.table.th.operation': 'Операция',
|
||
'usage.table.th.protocol': 'Протокол',
|
||
'usage.table.th.calls': 'Вызовы',
|
||
'usage.table.th.errors': 'Ошибки',
|
||
'usage.table.th.error_rate': 'Доля ошибок',
|
||
'usage.table.th.latency': 'Задержка (p50 / p95 / p99)',
|
||
'usage.table.th.share': 'Доля трафика',
|
||
'usage.table.empty.title': 'Разбивки по операциям пока нет',
|
||
'usage.table.empty.sub': 'За выбранный период нет вызовов, которые можно разложить по операциям.',
|
||
'usage.table.share': '{value}% трафика воркспейса',
|
||
'usage.loading.title': 'Загрузка использования…',
|
||
'usage.loading.sub': 'Собираем метрики вызовов для выбранного воркспейса.',
|
||
'usage.error.title': 'Не удалось загрузить использование',
|
||
'usage.error.api': 'Admin API недоступен',
|
||
'usage.error.workspace': 'Воркспейс не выбран',
|
||
'usage.error.load': 'Не удалось загрузить метрики использования',
|
||
'usage.export.empty.title': 'Данные по использованию не загружены',
|
||
'usage.export.empty.body': 'Сначала загрузите данные использования, а потом экспортируйте снимок CSV.',
|
||
'usage.export.done.title': 'Использование экспортировано',
|
||
'usage.export.done.body': 'Текущий снимок использования экспортирован в CSV.',
|
||
'usage.chart.ok': 'Успешных: {count}',
|
||
'usage.chart.errors': 'Ошибок: {count}',
|
||
'usage.chart.week': 'Нед. {index}',
|
||
'usage.csv.header': 'Операция,Протокол,Вызовы,Ошибки,Доля ошибок (%),p50 (мс),p95 (мс),p99 (мс)',
|
||
|
||
// Settings sidebar
|
||
'settings.title': 'Настройки',
|
||
'settings.nav.workspace': 'Воркспейс',
|
||
'settings.nav.profile': 'Профиль',
|
||
'settings.nav.security': 'Безопасность',
|
||
'settings.nav.danger': 'Опасная зона',
|
||
|
||
// Settings — workspace section
|
||
'settings.ws.title': 'Настройки воркспейса',
|
||
'settings.ws.subtitle': 'Общая информация о вашем воркспейсе',
|
||
'settings.ws.name': 'Имя воркспейса',
|
||
'settings.ws.display': 'Отображаемое имя',
|
||
'settings.ws.desc': 'Описание',
|
||
|
||
// Settings — language
|
||
'settings.lang.title': 'Язык',
|
||
'settings.lang.subtitle': 'Язык отображения интерфейса',
|
||
'settings.lang.en': 'English',
|
||
'settings.lang.ru': 'Русский',
|
||
|
||
// Settings — profile section
|
||
'settings.profile.title': 'Профиль',
|
||
'settings.profile.name': 'Полное имя',
|
||
'settings.profile.email': 'Email',
|
||
'settings.profile.role': 'Роль',
|
||
'settings.page.title': 'Настройки аккаунта',
|
||
'settings.ws.slug_hint': 'Используется в API endpoint-ах и в интерфейсе. Допустимы только английские строчные буквы, цифры и дефисы.',
|
||
'settings.ws.description_optional': 'Описание (необязательно)',
|
||
'settings.ws.description_placeholder': 'Какие задачи выполняет воркспейс?',
|
||
'settings.profile.avatar_hint': 'Аватар строится из отображаемого имени и email.',
|
||
'settings.profile.first_name': 'Имя',
|
||
'settings.profile.last_name': 'Фамилия',
|
||
'settings.profile.last_name_placeholder': 'Необязательно',
|
||
'settings.profile.email_label': 'Email адрес',
|
||
'settings.profile.email_hint': 'Изменение email обновляет логин для текущего аккаунта.',
|
||
'settings.profile.save': 'Сохранить профиль',
|
||
'settings.profile.saving': 'Сохранение…',
|
||
'settings.profile.saved': 'Сохранено ✓',
|
||
'settings.profile.saved_status': 'Профиль сохранен.',
|
||
'settings.profile.save_error': 'Не удалось сохранить профиль',
|
||
'settings.security.title': 'Безопасность',
|
||
'settings.security.current_password': 'Текущий пароль',
|
||
'settings.security.new_password': 'Новый пароль',
|
||
'settings.security.new_password_placeholder': 'минимум 12 символов',
|
||
'settings.security.confirm_password': 'Подтвердите новый пароль',
|
||
'settings.security.change_password': 'Сменить пароль',
|
||
'settings.security.mismatch': 'Новый пароль и подтверждение не совпадают.',
|
||
'settings.security.saved': 'Пароль обновлен.',
|
||
'settings.security.save_error': 'Не удалось изменить пароль',
|
||
'settings.capability.rest': 'REST / HTTP',
|
||
'settings.capability.standard': 'standard',
|
||
'settings.capability.elevated': 'elevated',
|
||
'settings.capability.strict': 'strict',
|
||
'settings.capability.static_agent_key': 'статические ключи агентов',
|
||
'settings.capability.unlimited': 'без лимита',
|
||
'settings.capability.none': 'нет',
|
||
'settings.session.title': 'Текущая сессия',
|
||
'settings.session.browser': 'Сессия браузера',
|
||
'settings.session.current': 'текущая',
|
||
'settings.session.loading': 'Загрузка деталей текущей сессии…',
|
||
'settings.session.unavailable': 'Детали сессии недоступны. Используйте «Выйти», чтобы отозвать текущую browser session.',
|
||
'settings.session.current_workspace': 'текущий воркспейс',
|
||
'settings.session.summary': '{email} · {role} в {workspace}. Используйте «Выйти», чтобы отозвать текущую browser session.',
|
||
'settings.planned_badge': 'Запланировано',
|
||
'settings.workspace.saving': 'Сохранение…',
|
||
'settings.workspace.saved': 'Сохранено ✓',
|
||
'settings.workspace.save_error': 'Не удалось сохранить настройки воркспейса',
|
||
'settings.workspace.save_error_title': 'Не удалось сохранить воркспейс',
|
||
|
||
// Workspace setup
|
||
'workspace_setup.title': 'Настройки воркспейса',
|
||
'workspace_setup.page_subtitle': 'Настройте идентичность вашего self-hosted воркспейса.',
|
||
'workspace_setup.back': 'Назад',
|
||
'workspace_setup.identity.title': 'Идентичность воркспейса',
|
||
'workspace_setup.identity.avatar_hint': 'Аватар формируется из имени воркспейса.',
|
||
'workspace_setup.name': 'Имя воркспейса',
|
||
'workspace_setup.slug': 'Slug',
|
||
'workspace_setup.required': 'обязательно',
|
||
'workspace_setup.optional': '(необязательно)',
|
||
'workspace_setup.slug_hint': 'Только строчные буквы, цифры и дефисы. Используется в URL MCP endpoint-ов.',
|
||
'workspace_setup.description': 'Описание',
|
||
'workspace_setup.description_placeholder': 'Какие задачи выполняет воркспейс?',
|
||
'workspace_setup.actions.save': 'Сохранить изменения',
|
||
'workspace_setup.create.title': 'Создать новый воркспейс',
|
||
'workspace_setup.create.subtitle': 'В Community используется один воркспейс для MCP-операций и агентов.',
|
||
'workspace_setup.create.footer': 'В Community используется один воркспейс.',
|
||
'workspace_setup.danger.title': 'Опасная зона',
|
||
'workspace_setup.danger.export_title': 'Экспортировать все данные',
|
||
'workspace_setup.danger.export_body': 'Скачать JSON-снимок настроек воркспейса, операций, агентов, секретов, данных использования и ключей доступа агентов.',
|
||
'workspace_setup.export': 'Экспорт',
|
||
'workspace_setup.role.owner': 'Владелец',
|
||
'workspace_setup.role.admin': 'Администратор',
|
||
'workspace_setup.role.operator': 'Разработчик',
|
||
'workspace_setup.role.viewer': 'Наблюдатель',
|
||
'workspace_setup.saving_create': 'Создание…',
|
||
'workspace_setup.saving_update': 'Сохранение…',
|
||
'workspace_setup.saved': 'Сохранено ✓',
|
||
'workspace_setup.save_error': 'Не удалось сохранить воркспейс',
|
||
'workspace_setup.save_error_title': 'Не удалось сохранить',
|
||
'workspace_setup.export_error': 'Не удалось экспортировать воркспейс',
|
||
'workspace_setup.export_error_title': 'Не удалось экспортировать',
|
||
'workspace.switch_error': 'Не удалось переключить воркспейс',
|
||
'workspace.switch_error_title': 'Не удалось переключить воркспейс',
|
||
'workspace_setup.custom_protocol.rest': 'REST',
|
||
|
||
// Wizard
|
||
'wizard.back_catalog': 'Операции',
|
||
'wizard.progress.create': 'Создать операцию',
|
||
'wizard.progress.edit': 'Редактировать операцию',
|
||
'wizard.exit': 'Выйти из мастера',
|
||
'wizard.step_of': 'Шаг <strong>{step}</strong> из {total}',
|
||
'wizard.step_short': 'Шаг {step}',
|
||
'wizard.status.completed': 'Завершено',
|
||
'wizard.status.in_progress': 'В процессе',
|
||
'wizard.status.not_started': 'Не начато',
|
||
'wizard.help.title': 'Нужна помощь?',
|
||
'wizard.help.body': 'Не уверены, какой протокол выбрать? Откройте',
|
||
'wizard.help.link': 'гайд по сравнению протоколов →',
|
||
'wizard.progress.section': 'Прогресс',
|
||
'wizard.step_name.protocol': 'Протокол',
|
||
'wizard.step_name.upstream': 'API-хост',
|
||
'wizard.step_name.tool': 'Настройки инструмента',
|
||
'wizard.step_name.mapping': 'Связи полей',
|
||
'wizard.button.back': 'Назад',
|
||
'wizard.button.continue': 'Продолжить',
|
||
'wizard.button.save_draft': 'Сохранить черновик',
|
||
'wizard.button.create': 'Создать операцию',
|
||
'wizard.button.save_changes': 'Сохранить изменения',
|
||
'wizard.sidebar.brand.create': 'Создать <span>операцию</span>',
|
||
'wizard.sidebar.brand.edit': 'Редактировать <span>операцию</span>',
|
||
'wizard.step1.title': 'Выберите протокол',
|
||
'wizard.step1.subtitle': 'Выберите протокол вашего API. От этого зависит, как Crank будет выполнять запросы и какие настройки будут доступны на следующих шагах.',
|
||
'wizard.step1.rest_name': 'REST / HTTP',
|
||
'wizard.step1.rest_tagline': 'Стандартные HTTP-методы для REST API. Самый универсальный вариант.',
|
||
'wizard.step1.query': 'Query',
|
||
'wizard.step1.mutation': 'Mutation',
|
||
'wizard.step1.unary': 'Unary',
|
||
'wizard.step1.tip_title': 'Это можно изменить позже',
|
||
'wizard.step1.tip_body': 'Если сменить протокол после настройки следующих шагов, схемы и связи полей будут очищены. Перед экспериментами сохраните операцию.',
|
||
'wizard.step1.community_protocol_title': 'Граница протоколов Community',
|
||
'wizard.step5.streaming_title': 'Потоковое выполнение',
|
||
'wizard.step2.title': 'Выберите API-хост и путь',
|
||
'wizard.step2.subtitle': 'Выберите существующий API-хост или добавьте новый. Затем задайте путь, который будет вызывать эта операция.',
|
||
'wizard.required': 'Обязательно',
|
||
'wizard.optional': 'Необязательно',
|
||
'wizard.step2.upstream_title': 'API-хост',
|
||
'wizard.step2.upstream_subtitle': 'Базовый URL и способ авторизации',
|
||
'wizard.step2.upstream_placeholder': 'Выберите API-хост…',
|
||
'wizard.step2.search_placeholder': 'Поиск по имени или URL…',
|
||
'wizard.step2.change': 'Изменить',
|
||
'wizard.step2.auth_none': 'Без авторизации',
|
||
'wizard.step2.auth_bearer': 'Bearer-токен',
|
||
'wizard.step2.auth_basic': 'Логин и пароль',
|
||
'wizard.step2.auth_apikey': 'API-ключ',
|
||
'wizard.step2.register_new': 'Добавить новый API-хост',
|
||
'wizard.step2.name': 'Имя',
|
||
'wizard.step2.base_url': 'Базовый URL',
|
||
'wizard.step2.unique_hint': 'Уникальное имя этого API-хоста.',
|
||
'wizard.step2.base_url_hint': 'Корневой URL без завершающего слеша. Авторизация настраивается ниже.',
|
||
'wizard.step2.auth_selector': 'Авторизация API-хоста',
|
||
'wizard.step2.auth_selector_hint': 'Используйте секреты, чтобы не хранить токены и пароли прямо в настройках API-хоста.',
|
||
'wizard.step2.auth_mode.none': 'Без авторизации',
|
||
'wizard.step2.auth_mode.existing': 'Использовать существующий профиль авторизации',
|
||
'wizard.step2.auth_mode.create': 'Создать профиль авторизации сейчас',
|
||
'wizard.step2.auth_profile': 'Профиль авторизации',
|
||
'wizard.step2.auth_profile_hint': 'Выбранный профиль применяется автоматически при каждом вызове инструмента.',
|
||
'wizard.step2.auth_profile_empty': 'Профилей авторизации пока нет. Создайте профиль ниже.',
|
||
'wizard.step2.create_profile_title': 'Создать профиль авторизации',
|
||
'wizard.step2.create_profile_subtitle': 'Профиль можно переиспользовать в нескольких API-хостах. Значения берутся из зашифрованных секретов воркспейса.',
|
||
'wizard.step2.profile_name': 'Имя профиля',
|
||
'wizard.step2.profile_kind': 'Тип авторизации',
|
||
'wizard.step2.auth_kind.bearer': 'Bearer-токен',
|
||
'wizard.step2.auth_kind.basic': 'Логин и пароль',
|
||
'wizard.step2.auth_kind.api_key_header': 'API-ключ в заголовке',
|
||
'wizard.step2.auth_kind.api_key_query': 'API-ключ в параметре запроса',
|
||
'wizard.step2.header_name': 'Имя заголовка',
|
||
'wizard.step2.query_param': 'Параметр запроса',
|
||
'wizard.step2.username_secret': 'Секрет логина',
|
||
'wizard.step2.password_secret': 'Секрет пароля',
|
||
'wizard.step2.secret_value': 'Секрет',
|
||
'wizard.step2.quick_secret': 'Быстро создать секрет',
|
||
'wizard.step2.manage_secrets': 'Открыть страницу секретов',
|
||
'wizard.step2.static_headers': 'Статические заголовки (опционально)',
|
||
'wizard.step2.static_headers_hint': 'Необязательные заголовки без секретных значений. Токены, пароли и ключи храните через профиль авторизации.',
|
||
'wizard.step2.quick_secret_title': 'Быстро создать секрет',
|
||
'wizard.step2.quick_secret_name': 'Имя секрета',
|
||
'wizard.step2.quick_secret_kind': 'Тип секрета',
|
||
'wizard.step2.quick_secret_value': 'Значение секрета',
|
||
'wizard.step2.quick_secret_hint': 'Значение сразу шифруется и больше не отображается.',
|
||
'wizard.step2.quick_secret_submit': 'Создать секрет',
|
||
'wizard.step2.save_upstream': 'Сохранить API-хост',
|
||
'wizard.step2.endpoint_title': 'Путь API',
|
||
'wizard.step2.endpoint_subtitle': 'Путь и HTTP-метод для конкретной операции',
|
||
'wizard.step2.path_template': 'Шаблон пути',
|
||
'wizard.step2.path_hint': 'Добавляется к базовому URL API-хоста. Используйте {param} для параметров пути.',
|
||
'wizard.step3.rest.label': 'Настройки запроса',
|
||
'wizard.step3.rest.title': 'HTTP метод и формат',
|
||
'wizard.step3.rest.subtitle': 'Выберите HTTP-метод и формат запроса. Crank подставит параметры MCP инструмента в запрос к API.',
|
||
'wizard.step3.rest.method_title': 'HTTP метод',
|
||
'wizard.step3.rest.method_subtitle': 'Метод, который отправляется в API при каждом вызове инструмента',
|
||
'wizard.step3.rest.read': 'Чтение',
|
||
'wizard.step3.rest.create': 'Создание',
|
||
'wizard.step3.rest.replace': 'Замена',
|
||
'wizard.step3.rest.update': 'Обновление',
|
||
'wizard.step3.rest.remove': 'Удаление',
|
||
'wizard.step3.rest.format_title': 'Формат запроса',
|
||
'wizard.step3.rest.format_subtitle': 'Формат тела запроса и ожидаемого ответа',
|
||
'wizard.step3.rest.content_type': 'Content-Type',
|
||
'wizard.step3.rest.content_type_hint': 'В каком формате отправлять данные в API.',
|
||
'wizard.step3.rest.accept': 'Accept',
|
||
'wizard.step3.rest.accept_hint': 'Какой формат ответа ожидать от API.',
|
||
'wizard.step4.title': 'Настройки инструмента',
|
||
'wizard.step4.subtitle': 'Задайте имя инструмента, описание для LLM и определите входную/выходную схему. Описание — главный сигнал, по которому модель решает, вызывать ли этот инструмент.',
|
||
'wizard.step4.identity_title': 'Параметры инструмента',
|
||
'wizard.step4.identity_subtitle': 'Имя инструмента и описание его назначения',
|
||
'wizard.step4.tool_name': 'Имя инструмента',
|
||
'wizard.step4.tool_name_hint': 'Техническое имя для вызова MCP инструмента. Допустимы только строчные английские буквы, цифры и подчеркивания. После публикации изменить нельзя.',
|
||
'wizard.step4.display_name': 'Отображаемое имя',
|
||
'wizard.step4.display_name_hint': 'Понятное название, которое отображается в интерфейсе и логах.',
|
||
'wizard.step4.tool_title': 'Короткое описание',
|
||
'wizard.step4.tool_title_hint': 'Короткая фраза, которая будет показана MCP клиенту в описании инструмента.',
|
||
'wizard.step4.description': 'Описание',
|
||
'wizard.step4.description_hint': 'Описание для модели. Пишите точно: по этому тексту модель решает, когда нужно вызвать инструмент.',
|
||
'wizard.step4.description_title': 'Как писать хорошие описания',
|
||
'wizard.step4.description_body': 'Начинайте с глагола: создает, получает, обновляет. Укажите обязательные входные данные и опишите успешный результат. Избегайте расплывчатых формулировок вроде “обрабатывает” или “управляет”.',
|
||
'wizard.step4.input_schema_title': 'Входная схема',
|
||
'wizard.step4.input_schema_subtitle': 'Параметры, которые LLM передает при вызове инструмента',
|
||
'wizard.step4.output_schema_title': 'Выходная схема',
|
||
'wizard.step4.output_schema_subtitle': 'Форма данных, которые LLM получает после успешного вызова',
|
||
'wizard.step4.schema_draft': 'JSON Schema draft-07',
|
||
'wizard.step4.protocol_agnostic_title': 'Схемы описывают инструмент',
|
||
'wizard.step4.protocol_agnostic_body': 'Входная схема описывает данные, которые MCP клиент передает инструменту. Выходная схема описывает результат работы инструмента. На следующем шаге эти поля связываются с реальным API-запросом и ответом.',
|
||
'wizard.step5.title': 'Связи полей и запуск',
|
||
'wizard.step5.subtitle': 'Свяжите параметры MCP инструмента с полями API-запроса, а ответ API с результатом инструмента. Авторизация настраивается на шаге 2.',
|
||
'wizard.step5.input_request': 'Вход инструмента → API-запрос',
|
||
'wizard.step5.output_response': 'Ответ API → результат инструмента',
|
||
'wizard.step5.execution': 'Параметры выполнения',
|
||
'wizard.step5.exec_title': 'Выполнение запроса',
|
||
'wizard.step5.exec_subtitle': 'Время ожидания, повторные попытки и профиль авторизации',
|
||
'wizard.approval.title': 'Подтверждение человеком',
|
||
'wizard.approval.subtitle': 'Включайте для действий, которые нельзя выполнять без явного решения пользователя.',
|
||
'wizard.approval.required_label': 'Требовать подтверждение перед выполнением',
|
||
'wizard.approval.required_desc': 'MCP клиент получит ожидающий запрос, а действие выполнится только после подтверждения через отдельный эндпоинт подтверждения.',
|
||
'wizard.approval.risk_level': 'Уровень риска',
|
||
'wizard.approval.risk.normal': 'Обычное действие',
|
||
'wizard.approval.risk.dangerous': 'Опасное действие',
|
||
'wizard.approval.risk.financial': 'Финансовое действие',
|
||
'wizard.approval.risk.irreversible': 'Необратимое действие',
|
||
'wizard.approval.ttl': 'Сколько ждать подтверждение',
|
||
'wizard.approval.confirmation_title': 'Заголовок подтверждения',
|
||
'wizard.approval.confirmation_title_hint': 'Этот текст увидит внешний интерфейс подтверждения.',
|
||
'wizard.approval.confirmation_body': 'Описание для пользователя',
|
||
'wizard.approval.confirmation_body_hint': 'Коротко объясните, что произойдет после подтверждения.',
|
||
'wizard.approval.show_payload': 'Показывать параметры запроса',
|
||
'wizard.approval.payload_mode': 'Как показывать параметры',
|
||
'wizard.approval.payload.summary': 'Краткое описание',
|
||
'wizard.approval.payload.masked_json': 'JSON с маскированием секретов',
|
||
'wizard.approval.preview_label': 'Предпросмотр для интерфейса подтверждения',
|
||
'wizard.approval.default_title': 'Подтвердите выполнение операции',
|
||
'wizard.approval.default_body': 'Проверьте параметры операции и подтвердите выполнение.',
|
||
'wizard.step5.security_level_title': 'Защита операции',
|
||
'wizard.step5.community_security_note': '',
|
||
'wizard.step5.live_title': 'Проверка и публикация',
|
||
'wizard.step5.draft_actions': 'Результат действия',
|
||
'wizard.step5.live_save_title': 'Сохранение перед действием',
|
||
'wizard.step5.live_save_body': 'Перед проверкой или публикацией Crank сохраняет текущие настройки.',
|
||
'wizard.step5.agent_preview_title': 'Как инструмент увидит MCP клиент',
|
||
'wizard.step5.agent_preview_subtitle': 'Проверьте имя, описание, входную схему и примеры до публикации.',
|
||
'wizard.step5.copy_agent_preview': 'Копировать схему',
|
||
'wizard.step5.preview_tool_name': 'Имя инструмента',
|
||
'wizard.step5.preview_tool_title': 'Короткое описание',
|
||
'wizard.step5.preview_tool_description': 'Описание для модели',
|
||
'wizard.step5.preview_input_schema': 'Входная схема',
|
||
'wizard.step5.preview_tool_call': 'Пример вызова tools/call',
|
||
'wizard.step5.preview_success_response': 'Пример успешного ответа',
|
||
'wizard.step5.preview_error_response': 'Пример ошибки',
|
||
'wizard.step5.samples_title': 'Примеры и автоматическая настройка',
|
||
'wizard.step5.samples_subtitle': 'Сохраните пример входных и выходных данных, затем Crank сможет пересобрать схемы и связи полей на их основе.',
|
||
'wizard.step5.input_sample': 'Пример входных данных',
|
||
'wizard.step5.output_sample': 'Пример ответа',
|
||
'wizard.step5.save_input_sample': 'Сохранить входной пример',
|
||
'wizard.step5.save_output_sample': 'Сохранить выходной пример',
|
||
'wizard.step5.generate_draft': 'Пересобрать по примерам',
|
||
'wizard.step5.test_title': 'Тестирование операции',
|
||
'wizard.step5.test_subtitle': 'Выполните инструмент на реальном API перед публикацией.',
|
||
'wizard.step5.test_input_payload': 'Входные данные для теста',
|
||
'wizard.step5.run_test': 'Запустить тест',
|
||
'wizard.step5.use_response_output': 'Использовать ответ как выходной пример',
|
||
'wizard.step5.request_preview': 'Предпросмотр API-запроса',
|
||
'wizard.step5.response_preview': 'Предпросмотр ответа API',
|
||
'wizard.step5.errors': 'Ошибки',
|
||
'wizard.step5.choose_descriptor': 'Выбрать descriptor set',
|
||
'wizard.step5.upload_discover': 'Загрузить и открыть сервисы',
|
||
'wizard.step5.no_descriptor': 'Descriptor set не выбран',
|
||
'wizard.step5.yaml_title': 'Импорт и экспорт YAML',
|
||
'wizard.step5.yaml_subtitle': 'Экспортируйте операцию в файл или импортируйте подготовленную YAML-конфигурацию.',
|
||
'wizard.step5.export_yaml': 'Экспорт YAML',
|
||
'wizard.step5.load_yaml_file': 'Загрузить YAML-файл',
|
||
'wizard.step5.import_yaml': 'Импорт YAML',
|
||
'wizard.step5.yaml_document': 'YAML-конфигурация',
|
||
'wizard.step5.yaml_placeholder': 'Вставьте YAML сюда, чтобы создать или обновить операцию.',
|
||
'wizard.step5.publish_title': 'Публикация',
|
||
'wizard.step5.publish_subtitle': 'Опубликуйте операцию, чтобы ее можно было добавить агентам.',
|
||
'wizard.step5.publish_hint': 'Перед публикацией Crank сохраняет текущие настройки из мастера.',
|
||
'wizard.step5.publish_action': 'Опубликовать операцию',
|
||
'wizard.step5.editable_title': 'Настройки можно изменить позже',
|
||
'wizard.step5.editable_body': 'Сохранение обновляет настройки операции в этом мастере.',
|
||
'wizard.live.failed_title': 'Не удалось выполнить действие',
|
||
'wizard.live.failed_body': 'Запрос завершился ошибкой. Проверьте настройки операции и попробуйте еще раз.',
|
||
'wizard.error.no_workspace': 'Воркспейс не выбран',
|
||
'wizard.error.tool_name': 'Имя инструмента обязательно',
|
||
'wizard.error.parser_unavailable': 'YAML parser недоступен',
|
||
'wizard.error.select_upstream': 'Сначала выберите или добавьте API-хост',
|
||
'wizard.error.auth_profile_required': 'Выберите профиль авторизации или переключите авторизацию API-хоста в режим «Без авторизации».',
|
||
'wizard.error.auth_profile_name': 'Нужно указать имя профиля авторизации.',
|
||
'wizard.error.secret_required': 'Выберите секрет для этого профиля авторизации.',
|
||
'wizard.error.basic_secrets_required': 'Выберите секреты и для логина, и для пароля.',
|
||
'wizard.error.header_name_required': 'Для этого профиля авторизации нужно указать имя заголовка.',
|
||
'wizard.error.query_param_required': 'Для этого профиля авторизации нужно указать имя query-параметра.',
|
||
'wizard.error.secret_name_required': 'Нужно указать имя секрета.',
|
||
'wizard.error.secret_value_required': 'Нужно указать значение секрета.',
|
||
'wizard.error.save_upstream_first': 'Сначала сохраните API-хост после создания нового профиля авторизации.',
|
||
'wizard.toast.auth_profile_created_title': 'Профиль авторизации создан',
|
||
'wizard.toast.auth_profile_created_body': 'Новый профиль авторизации готов и уже выбран.',
|
||
'wizard.toast.auth_profile_error_title': 'Не удалось создать профиль авторизации',
|
||
'wizard.toast.quick_secret_created_title': 'Секрет создан',
|
||
'wizard.toast.quick_secret_created_body': 'Новый секрет уже доступен в выборе авторизации.',
|
||
'wizard.toast.quick_secret_error_title': 'Не удалось создать секрет',
|
||
'wizard.error.save': 'Не удалось сохранить операцию',
|
||
'wizard.error.save_title': 'Не удалось сохранить',
|
||
'wizard.save.created': 'Черновик создан',
|
||
'wizard.save.updated': 'Черновик обновлен',
|
||
'wizard.save.body': 'Текущие настройки операции сохранены.',
|
||
'wizard.save.saved': 'Сохранено ✓',
|
||
'wizard.busy.input_sample': 'Сохранение входного примера…',
|
||
'wizard.busy.output_sample': 'Сохранение выходного примера…',
|
||
'wizard.busy.generate': 'Пересборка настроек…',
|
||
'wizard.busy.test': 'Запуск теста…',
|
||
'wizard.busy.quality': 'Проверка качества…',
|
||
'wizard.busy.export_yaml': 'Экспорт YAML…',
|
||
'wizard.busy.import_yaml': 'Импорт YAML…',
|
||
'wizard.busy.publish': 'Публикация операции…',
|
||
'wizard.busy.descriptor': 'Загрузка descriptor set…',
|
||
'wizard.busy.wsdl': 'Проверка WSDL…',
|
||
'wizard.sample.input_saved': 'Входной пример сохранен',
|
||
'wizard.sample.input_saved_body': 'Входной пример сохранен для этой операции.',
|
||
'wizard.sample.output_saved': 'Выходной пример сохранен',
|
||
'wizard.sample.output_saved_body': 'Выходной пример сохранен для этой операции.',
|
||
'wizard.sample.generated': 'Настройки пересобраны',
|
||
'wizard.sample.generated_body': 'Схемы и связи полей пересобраны по сохраненным примерам.',
|
||
'wizard.test.completed': 'Тестирование операции завершено',
|
||
'wizard.test.failed': 'Тестирование операции вернуло ошибки',
|
||
'wizard.test.completed_body': 'Операция успешно выполнилась на API.',
|
||
'wizard.test.failed_body': 'Операция выполнилась, но API или валидация вернули ошибки.',
|
||
'wizard.test.window_completed': 'Оконный тест завершен',
|
||
'wizard.test.window_completed_body': 'Собран ограниченный результат окна.',
|
||
'wizard.test.window_truncated_note': 'Результат был усечен.',
|
||
'wizard.test.window_more_note': 'Доступно продолжение, если перейти к streaming-представлениям.',
|
||
'wizard.test.window_incomplete_note': 'Upstream-поток завершился раньше, чем удалось собрать всё окно.',
|
||
'wizard.test.no_response': 'Тестового ответа пока нет',
|
||
'wizard.test.no_response_body': 'Сначала выполните тест, затем скопируйте ответ в выходной пример.',
|
||
'wizard.test.response_copied': 'Ответ скопирован',
|
||
'wizard.test.response_copied_body': 'Последний тестовый ответ скопирован в выходной пример.',
|
||
'wizard.agent_preview.copied': 'Схема скопирована',
|
||
'wizard.agent_preview.copied_body': 'Схема MCP инструмента скопирована в буфер обмена.',
|
||
'wizard.quality.title': 'Проверка качества инструмента',
|
||
'wizard.quality.subtitle': 'Проверьте имя, описание, схемы и результат, который увидит агент.',
|
||
'wizard.quality.action': 'Проверить качество',
|
||
'wizard.quality.empty': 'Проверка еще не запускалась.',
|
||
'wizard.quality.no_findings': 'Замечаний нет.',
|
||
'wizard.quality.checked_title': 'Проверка качества завершена',
|
||
'wizard.quality.checked_body': 'Посмотрите рекомендации перед публикацией.',
|
||
'wizard.quality.blocked_title': 'Проверка нашла блокирующие ошибки',
|
||
'wizard.quality.blocked_body': 'Исправьте замечания уровня «Ошибка» перед публикацией.',
|
||
'wizard.quality.blocking_error': 'Исправьте блокирующие замечания качества перед публикацией.',
|
||
'wizard.quality.severity_error': 'Ошибка',
|
||
'wizard.quality.severity_warning': 'Предупреждение',
|
||
'wizard.quality.severity_info': 'Информация',
|
||
'wizard.boolean.yes': 'да',
|
||
'wizard.boolean.no': 'нет',
|
||
'wizard.yaml.exported': 'YAML экспортирован',
|
||
'wizard.yaml.exported_body': 'Текущая конфигурация операции скачана в YAML.',
|
||
'wizard.yaml.none': 'YAML не предоставлен',
|
||
'wizard.yaml.none_body': 'Вставьте YAML-документ или загрузите файл перед импортом.',
|
||
'wizard.yaml.imported': 'YAML импортирован',
|
||
'wizard.yaml.imported_with_warnings': 'YAML импортирован с рекомендациями',
|
||
'wizard.yaml.imported_body': 'Импортированная операция сохранена. Мастер сейчас откроет ее заново.',
|
||
'wizard.yaml.loaded': 'YAML загружен',
|
||
'wizard.yaml.loaded_body': 'YAML-конфигурация загружена.',
|
||
'wizard.publish.done': 'Операция опубликована',
|
||
'wizard.publish.done_body': 'Версия {version} опубликована и теперь может быть привязана к агентам.',
|
||
|
||
// Common buttons
|
||
'btn.save': 'Сохранить',
|
||
'btn.cancel': 'Отмена',
|
||
'btn.create': 'Создать',
|
||
'btn.delete': 'Удалить',
|
||
'btn.revoke': 'Отозвать',
|
||
'btn.copy': 'Копировать',
|
||
|
||
// Agents page
|
||
'agents.title': 'Агенты',
|
||
'agents.subtitle': 'Здесь можно сгруппировать ваши MCP инструменты по AI-агентам.',
|
||
'agents.subtitle.community': 'Здесь можно сгруппировать ваши MCP инструменты по AI-агентам.',
|
||
'agents.new': 'Новый агент',
|
||
'agents.limit.title': 'Достигнут лимит агентов',
|
||
'agents.limit.message': 'В этой редакции доступно не более {limit} AI-агента. Чтобы добавить еще одного, удалите существующего агента.',
|
||
'agents.stats.total': 'Всего агентов',
|
||
'agents.stats.total_sub': 'в этом воркспейсе',
|
||
'agents.stats.published': 'Опубликованы',
|
||
'agents.stats.published_sub': '{value}% от общего числа',
|
||
'agents.stats.tools': 'Открытых операций',
|
||
'agents.stats.tools_sub': 'в {count} агентах',
|
||
'agents.stats.calls': 'Вызовов сегодня',
|
||
'agents.stats.calls_sub': 'по всем агентам',
|
||
'agents.search': 'Поиск агентов...',
|
||
'agents.results': '{shown} из {total} агентов',
|
||
'agents.callout.default': 'Старайтесь группировать MCP инструменты под конкретные задачи конкретного агента, не давайте одной LLM большое количество инструментов разом, это может приводить к ошибкам и галлюцинациям. Для каждого агента задаются свои API ключи.',
|
||
'agents.callout.community': 'Старайтесь группировать MCP инструменты под конкретные задачи конкретного агента, не давайте одной LLM большое количество инструментов разом, это может приводить к ошибкам и галлюцинациям. Для каждого агента задаются свои API ключи.',
|
||
'agents.loading': 'Загрузка агентов…',
|
||
'agents.error.title': 'Не удалось загрузить агентов',
|
||
'agents.error.api': 'Воркспейс или API недоступен',
|
||
'agents.error.load': 'Не удалось загрузить агентов',
|
||
'agents.empty.initial.title': 'Агентов пока нет',
|
||
'agents.empty.initial.sub': 'Создайте первого агента, чтобы получить отдельный MCP endpoint с нужным набором инструментов.',
|
||
'agents.empty.initial.sub_community': 'Создайте первого агента, чтобы получить один отдельный MCP endpoint и его статические машинные ключи для конкретного набора инструментов.',
|
||
'agents.empty.initial.next_step': 'После создания выпустите отдельные машинные ключи для этого endpoint на странице API Keys.',
|
||
'agents.empty.search.title': 'Нет агентов по запросу "{query}"',
|
||
'agents.empty.search.sub': 'Попробуйте другой поисковый запрос.',
|
||
'agents.card.tools': 'инструментов',
|
||
'agents.card.keys': 'ключей',
|
||
'agents.card.calls_today': 'вызовов сегодня',
|
||
'agents.card.created': 'Создан {date}',
|
||
'agents.card.copy_endpoint': 'Скопировать endpoint',
|
||
'agents.card.endpoint_help': 'Данный эндпоинт требуется указать на стороне MCP клиента вместе с API ключом.',
|
||
'agents.card.endpoint_help_community': 'Данный эндпоинт требуется указать на стороне MCP клиента вместе с API ключом.',
|
||
'agents.action.edit': 'Редактировать',
|
||
'agents.action.archive': 'Архивировать',
|
||
'agents.action.delete': 'Удалить',
|
||
'agents.lifecycle.publish': 'Опубликовать',
|
||
'agents.lifecycle.unpublish': 'Снять с публикации',
|
||
'agents.lifecycle.restore_draft': 'Вернуть в черновик',
|
||
'agents.lifecycle.published': 'Опубликован',
|
||
'agents.lifecycle.archived': 'Архивирован',
|
||
'agents.lifecycle.draft': 'Черновик',
|
||
'agents.drawer.new_title': 'Новый агент',
|
||
'agents.drawer.edit_title': 'Редактировать агента',
|
||
'agents.drawer.new_sub': 'Опишите агента и выберите доступные для него MCP инструменты.',
|
||
'agents.drawer.new_sub_community': 'Опишите агента и выберите доступные для него MCP инструменты.',
|
||
'agents.drawer.edit_sub': 'Обновите настройки агента и выбор инструментов',
|
||
'agents.drawer.identity': 'Идентичность',
|
||
'agents.drawer.display_name': 'Отображаемое имя',
|
||
'agents.drawer.slug': 'Slug',
|
||
'agents.drawer.description': 'Описание',
|
||
'agents.drawer.optional': '(необязательно)',
|
||
'agents.drawer.required': 'обязательно',
|
||
'agents.drawer.status': 'Статус',
|
||
'agents.drawer.endpoint': 'MCP endpoint',
|
||
'agents.drawer.slug_hint': 'Slug используется как часть endpoint-а для идентификации агента.',
|
||
'agents.drawer.placeholder.name': 'Customer Support',
|
||
'agents.drawer.placeholder.slug': 'customer-support',
|
||
'agents.drawer.placeholder.description': 'Что делает этот агент? Для какого сценария он нужен?',
|
||
'agents.drawer.operations': 'Операции',
|
||
'agents.drawer.selected': 'Выбрано: {count}',
|
||
'agents.drawer.operations_sub': 'Выберите MCP инструменты, которые будут доступны для этого агента.',
|
||
'agents.drawer.operations_sub_community': 'Выберите MCP инструменты, которые будут доступны для этого агента.',
|
||
'agents.drawer.finding.title': 'Рекомендация',
|
||
'agents.drawer.finding.too_many_tools': 'У агента выбрано много инструментов. Оставьте только те, которые нужны для одной конкретной задачи.',
|
||
'agents.drawer.finding.similar_tools': 'Инструменты «{left}» и «{right}» похожи. Переименуйте их точнее или оставьте один вариант.',
|
||
'agents.drawer.filter_ops': 'Фильтр операций…',
|
||
'agents.drawer.ops_no_match': 'Нет операций по запросу "{query}"',
|
||
'agents.drawer.ops_selected': 'Выбрано операций: {count}',
|
||
'agents.drawer.clear_all': 'Очистить все',
|
||
'agents.drawer.recommendation': 'Сейчас выбрано {count} инструментов. LLM лучше работает, когда у агента меньше 15 инструментов. Подумайте о разбиении по сценариям.',
|
||
'agents.drawer.cancel': 'Отмена',
|
||
'agents.drawer.create': 'Создать агента',
|
||
'agents.drawer.save': 'Сохранить изменения',
|
||
'agents.toast.saved_title_create': 'Агент создан',
|
||
'agents.toast.saved_title_update': 'Агент обновлен',
|
||
'agents.toast.saved_message': '{name} сохранен, привязано инструментов: {count}.',
|
||
'agents.toast.save_error_title': 'Не удалось обновить агента',
|
||
'agents.toast.save_error_message': 'Не удалось сохранить агента',
|
||
'agents.toast.delete_confirm': 'Удалить этого агента? Действие нельзя отменить.',
|
||
'agents.toast.delete_title': 'Агент удален',
|
||
'agents.toast.delete_message': 'Агент {name} удален.',
|
||
'agents.toast.delete_error_title': 'Не удалось удалить',
|
||
'agents.toast.delete_error_message': 'Не удалось удалить агента',
|
||
'agents.toast.lifecycle_title': 'Жизненный цикл агента обновлен',
|
||
'agents.toast.lifecycle_publish': '{name} опубликован.',
|
||
'agents.toast.lifecycle_unpublish': '{name} возвращен в черновик.',
|
||
'agents.toast.lifecycle_archive': '{name} архивирован.',
|
||
'agents.toast.lifecycle_error_title': 'Не удалось обновить жизненный цикл',
|
||
'agents.toast.lifecycle_error_message': 'Не удалось обновить состояние агента',
|
||
'agents.toast.endpoint_title': 'MCP endpoint скопирован',
|
||
|
||
// Demo content
|
||
'demo.agent.currency-rates.display_name': 'Курсы валют',
|
||
'demo.operation.frankfurter_latest_rate.display_name': 'Последний курс валюты',
|
||
'demo.operation.frankfurter_latest_rate.description': 'Возвращает последний доступный курс валюты через Frankfurter.',
|
||
|
||
// Login
|
||
'login.title': 'Войти',
|
||
'login.subtitle': 'Добро пожаловать обратно',
|
||
'login.email': 'Email',
|
||
'login.password': 'Пароль',
|
||
'login.submit': 'Войти',
|
||
'login.email_label': 'Email адрес',
|
||
'login.email_placeholder': 'you@acme.com',
|
||
'login.password_only': 'Войдите с помощью email и пароля.',
|
||
'login.error.required': 'Введите email и пароль.',
|
||
'login.error.invalid': 'Неверный email или пароль. Попробуйте еще раз.',
|
||
'login.error.generic': 'Сейчас не удается войти. Попробуйте еще раз.',
|
||
}
|
||
};
|
||
|
||
Object.assign(TRANSLATIONS.en, {
|
||
|
||
|
||
});
|
||
|
||
Object.assign(TRANSLATIONS.ru, {
|
||
|
||
|
||
});
|
||
|
||
function t(key) {
|
||
var lang = localStorage.getItem('crank_lang') || 'en';
|
||
var tr = TRANSLATIONS[lang] || TRANSLATIONS.en;
|
||
return tr[key] !== undefined ? tr[key] : (TRANSLATIONS.en[key] !== undefined ? TRANSLATIONS.en[key] : key);
|
||
}
|
||
|
||
function tf(key, vars) {
|
||
return Object.keys(vars || {}).reduce(function(result, name) {
|
||
return result.replaceAll('{' + name + '}', vars[name]);
|
||
}, t(key));
|
||
}
|
||
|
||
function pluralForm(lang, count) {
|
||
if (typeof Intl !== 'undefined' && typeof Intl.PluralRules === 'function') {
|
||
return new Intl.PluralRules(lang).select(Number(count) || 0);
|
||
}
|
||
return Number(count) === 1 ? 'one' : 'other';
|
||
}
|
||
|
||
function tPlural(key, count, vars) {
|
||
var lang = localStorage.getItem('crank_lang') || 'en';
|
||
var tr = TRANSLATIONS[lang] || TRANSLATIONS.en;
|
||
var form = pluralForm(lang, count);
|
||
var fallbackForm = pluralForm('en', count);
|
||
var resolvedKey = tr[key + '.' + form] !== undefined
|
||
? key + '.' + form
|
||
: tr[key + '.other'] !== undefined
|
||
? key + '.other'
|
||
: TRANSLATIONS.en[key + '.' + fallbackForm] !== undefined
|
||
? key + '.' + fallbackForm
|
||
: TRANSLATIONS.en[key + '.other'] !== undefined
|
||
? key + '.other'
|
||
: key;
|
||
var payload = Object.assign({ count: count }, vars || {});
|
||
return Object.keys(payload).reduce(function(result, name) {
|
||
return result.replaceAll('{' + name + '}', payload[name]);
|
||
}, t(resolvedKey));
|
||
}
|
||
|
||
function hasTranslation(key) {
|
||
return t(key) !== key;
|
||
}
|
||
|
||
function localizeDemoAgent(agent) {
|
||
if (!agent || !agent.slug) {
|
||
return agent;
|
||
}
|
||
|
||
var displayNameKey = 'demo.agent.' + agent.slug + '.display_name';
|
||
var descriptionKey = 'demo.agent.' + agent.slug + '.description';
|
||
|
||
if (!hasTranslation(displayNameKey) && !hasTranslation(descriptionKey)) {
|
||
return agent;
|
||
}
|
||
|
||
return Object.assign({}, agent, {
|
||
display_name: hasTranslation(displayNameKey) ? t(displayNameKey) : agent.display_name,
|
||
description: hasTranslation(descriptionKey) ? t(descriptionKey) : agent.description,
|
||
});
|
||
}
|
||
|
||
function localizeDemoOperation(operation) {
|
||
if (!operation || !operation.name) {
|
||
return operation;
|
||
}
|
||
|
||
var displayNameKey = 'demo.operation.' + operation.name + '.display_name';
|
||
var descriptionKey = 'demo.operation.' + operation.name + '.description';
|
||
|
||
if (!hasTranslation(displayNameKey) && !hasTranslation(descriptionKey)) {
|
||
return operation;
|
||
}
|
||
|
||
var next = Object.assign({}, operation);
|
||
|
||
if (hasTranslation(displayNameKey)) {
|
||
next.display_name = t(displayNameKey);
|
||
if (Object.prototype.hasOwnProperty.call(operation, 'operation_display_name')) {
|
||
next.operation_display_name = t(displayNameKey);
|
||
}
|
||
}
|
||
|
||
if (hasTranslation(descriptionKey)) {
|
||
if (Object.prototype.hasOwnProperty.call(operation, 'description')) {
|
||
next.description = t(descriptionKey);
|
||
}
|
||
if (Object.prototype.hasOwnProperty.call(operation, 'tool_description')) {
|
||
next.tool_description = t(descriptionKey);
|
||
}
|
||
}
|
||
|
||
return next;
|
||
}
|
||
|
||
window.localizeDemoAgent = localizeDemoAgent;
|
||
window.localizeDemoOperation = localizeDemoOperation;
|
||
window.tPlural = tPlural;
|
||
|
||
function applyLang() {
|
||
// text content
|
||
document.querySelectorAll('[data-i18n]').forEach(function(el) {
|
||
el.textContent = t(el.getAttribute('data-i18n'));
|
||
});
|
||
// placeholder
|
||
document.querySelectorAll('[data-i18n-ph]').forEach(function(el) {
|
||
el.placeholder = t(el.getAttribute('data-i18n-ph'));
|
||
});
|
||
// title
|
||
document.querySelectorAll('[data-i18n-title]').forEach(function(el) {
|
||
el.title = t(el.getAttribute('data-i18n-title'));
|
||
});
|
||
// aria-label
|
||
document.querySelectorAll('[data-i18n-aria-label]').forEach(function(el) {
|
||
el.setAttribute('aria-label', t(el.getAttribute('data-i18n-aria-label')));
|
||
});
|
||
// highlight active language button in settings
|
||
var lang = localStorage.getItem('crank_lang') || 'en';
|
||
document.querySelectorAll('.lang-btn').forEach(function(btn) {
|
||
btn.classList.toggle('active', btn.dataset.lang === lang);
|
||
});
|
||
}
|
||
|
||
function setLang(lang) {
|
||
localStorage.setItem('crank_lang', lang);
|
||
applyLang();
|
||
// Notify Alpine components to re-render reactive getters
|
||
window.dispatchEvent(new CustomEvent('crank:langchange', { detail: { lang: lang } }));
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', applyLang);
|