ui: improve localization and plural handling

This commit is contained in:
a.tolmachev
2026-04-12 01:24:09 +03:00
parent 4dffad603f
commit fa75393e5f
5 changed files with 158 additions and 69 deletions
+15 -13
View File
@@ -2,39 +2,41 @@
## Current ## Current
### `feat/refactoring-plan` ### `feat/frontend-login-simplification`
Status: in_progress Status: in_progress
DoD: DoD:
- `__REFACTORING.md` documents the full remediation plan for registry modularization, sqlx compile-time validation, KDF correction, typed timestamps, ID display support, pool tuning, and error normalization - login page shows only working actions
- `__REFACTORING.md` also documents correlation IDs, rate limiting/backpressure, and a distributed `mcp-server` session store - developer-facing build note is removed from the production login surface
- `.gitignore` ignores local `__*.md` working notes and refactoring specs - planned login capabilities are either removed or collapsed into one compact "coming later" hint
- `TASKS.md` points to the refactoring track as the active work item - copy stays user-facing in both EN and RU
- existing password sign-in flow keeps working without visual regressions
## Next ## Next
- `feat/postgres-registry-modularization` - `feat/frontend-settings-honesty-pass`
## Backlog ## Backlog
- `feat/frontend-refactoring-plan`
- `feat/frontend-ux-review-plan`
- `feat/frontend-xss-hardening` - `feat/frontend-xss-hardening`
- `feat/frontend-i18n-remediation`
- `feat/frontend-login-simplification` - `feat/frontend-login-simplification`
- `feat/frontend-settings-honesty-pass` - `feat/frontend-settings-honesty-pass`
- `feat/frontend-command-palette-decision` - `feat/frontend-command-palette-decision`
- `feat/frontend-plural-rules`
- `feat/frontend-template-safety-cleanup`
- `feat/frontend-shell-unification` - `feat/frontend-shell-unification`
- `feat/frontend-wizard-modularization` - `feat/frontend-wizard-modularization`
- `feat/frontend-build-pipeline` - `feat/frontend-build-pipeline`
- `feat/frontend-css-state-cleanup`
- `feat/frontend-performance-polish` - `feat/frontend-performance-polish`
- `feat/postgres-registry-modularization` - `feat/frontend-observability-and-testability`
- `feat/sqlx-compile-time-verification`
- `feat/typed-timestamps`
- `feat/secret-crypto-hkdf`
- `feat/id-display-support` - `feat/id-display-support`
- `feat/postgres-pool-config` - `feat/postgres-pool-config`
- `feat/postgres-registry-modularization`
- `feat/sqlx-compile-time-verification`
- `feat/secret-crypto-hkdf`
- `feat/typed-timestamps`
- `feat/error-structure-normalization` - `feat/error-structure-normalization`
- `feat/correlation-id-propagation` - `feat/correlation-id-propagation`
- `feat/runtime-rate-limiting-and-backpressure` - `feat/runtime-rate-limiting-and-backpressure`
+105 -40
View File
@@ -104,7 +104,7 @@ var TRANSLATIONS = {
'apikeys.th.created':'CREATED', 'apikeys.th.created':'CREATED',
'apikeys.th.last': 'LAST USED', 'apikeys.th.last': 'LAST USED',
'apikeys.callout.title': 'Keys are only shown once.', 'apikeys.callout.title': 'Keys are only shown once.',
'apikeys.callout.body': 'Copy and store them securely immediately after creation — Crank stores only a hash. `Last used` updates after successful MCP authentication.', '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.title': 'Active keys',
'apikeys.active.subtitle': '{active} active · {revoked} revoked', 'apikeys.active.subtitle': '{active} active · {revoked} revoked',
'apikeys.search': 'Filter keys…', 'apikeys.search': 'Filter keys…',
@@ -163,7 +163,7 @@ var TRANSLATIONS = {
'secrets.subtitle': 'Encrypted upstream credentials for bearer tokens, basic auth, and API keys used by auth profiles.', 'secrets.subtitle': 'Encrypted upstream credentials for bearer tokens, basic auth, and API keys used by auth profiles.',
'secrets.new': 'Create secret', 'secrets.new': 'Create secret',
'secrets.callout.title': 'Secret plaintext is write-only.', 'secrets.callout.title': 'Secret plaintext is write-only.',
'secrets.callout.body': 'Crank encrypts secret values with the workspace master key. After create or rotate, the API returns only metadata, so this page focuses on lifecycle and usage references.', 'secrets.callout.body': 'Crank encrypts secret values with the workspace master key. After creation or rotation, the API returns only metadata, so this page focuses on secret management and where each secret is used.',
'secrets.active.title': 'Workspace secrets', 'secrets.active.title': 'Workspace secrets',
'secrets.active.subtitle': '{active} active · {total} total', 'secrets.active.subtitle': '{active} active · {total} total',
'secrets.search': 'Filter secrets…', 'secrets.search': 'Filter secrets…',
@@ -180,7 +180,8 @@ var TRANSLATIONS = {
'secrets.status.active': 'Active', 'secrets.status.active': 'Active',
'secrets.status.disabled': 'Disabled', 'secrets.status.disabled': 'Disabled',
'secrets.last_used.never': 'Never', 'secrets.last_used.never': 'Never',
'secrets.used_by_count': '{count} profiles', 'secrets.used_by_count.one': '{count} profile',
'secrets.used_by_count.other': '{count} profiles',
'secrets.loading': 'Loading…', 'secrets.loading': 'Loading…',
'secrets.empty.none': 'No secrets yet', 'secrets.empty.none': 'No secrets yet',
'secrets.empty.search': 'No secrets match your search', 'secrets.empty.search': 'No secrets match your search',
@@ -222,19 +223,20 @@ var TRANSLATIONS = {
'secrets.hint.token': 'Best for bearer tokens and simple API key values.', 'secrets.hint.token': 'Best for bearer tokens and simple API key values.',
'secrets.hint.username_password': 'Stores username and password as one encrypted payload.', 'secrets.hint.username_password': 'Stores username and password as one encrypted payload.',
'secrets.hint.header': 'Stores a raw header value. Header name is configured in the auth profile.', 'secrets.hint.header': 'Stores a raw header value. Header name is configured in the auth profile.',
'secrets.hint.generic': 'Stores arbitrary JSON for advanced future integrations.', 'secrets.hint.generic': 'Stores arbitrary JSON for custom integrations that need a structured secret payload.',
'secrets.validation.name_required': 'Secret name is required.', 'secrets.validation.name_required': 'Secret name is required.',
'secrets.validation.value_required': 'Secret value is required.', 'secrets.validation.value_required': 'Secret value is required.',
'secrets.validation.username_password_required': 'Username and password are both required.', 'secrets.validation.username_password_required': 'Username and password are both required.',
'secrets.validation.json_invalid': 'Generic secret payload must be valid JSON.', 'secrets.validation.json_invalid': 'Generic secret payload must be valid JSON.',
'secrets.profiles.title': 'Auth profile references', 'secrets.profiles.title': 'Auth profile references',
'secrets.profiles.subtitle': 'Profiles resolve secrets at runtime before upstream execution.', 'secrets.profiles.subtitle': 'Profiles resolve secrets at runtime before upstream execution.',
'secrets.profiles.subtitle_count': '{count} auth profiles', 'secrets.profiles.subtitle_count.one': '{count} auth profile',
'secrets.profiles.subtitle_count.other': '{count} auth profiles',
'secrets.profiles.error_title': 'Unable to load auth profile references', 'secrets.profiles.error_title': 'Unable to load auth profile references',
'secrets.profiles.loading_title': 'Loading auth profile references…', 'secrets.profiles.loading_title': 'Loading auth profile references…',
'secrets.profiles.loading_body': 'Fetching auth profiles for the current workspace.', 'secrets.profiles.loading_body': 'Fetching auth profiles for the current workspace.',
'secrets.profiles.empty_title': 'No auth profiles yet', 'secrets.profiles.empty_title': 'No auth profiles yet',
'secrets.profiles.empty_body': 'Create auth profiles in the next slice to bind secrets to upstream authentication.', 'secrets.profiles.empty_body': 'Create auth profiles in the Wizard when configuring an operation.',
'secrets.profiles.references': 'Referenced secrets', 'secrets.profiles.references': 'Referenced secrets',
'secrets.profiles.reference_count': '{count} refs', 'secrets.profiles.reference_count': '{count} refs',
'secrets.profiles.meta.created': 'Created', 'secrets.profiles.meta.created': 'Created',
@@ -262,7 +264,7 @@ var TRANSLATIONS = {
'logs.paused': 'Paused', 'logs.paused': 'Paused',
'logs.refresh': 'Refresh', 'logs.refresh': 'Refresh',
'logs.range.30m': 'Last 30 min', 'logs.range.30m': 'Last 30 min',
'logs.range.1h': 'Last 1 hour', 'logs.range.1h': 'Last hour',
'logs.range.6h': 'Last 6 hours', 'logs.range.6h': 'Last 6 hours',
'logs.range.24h': 'Last 24 hours', 'logs.range.24h': 'Last 24 hours',
'logs.range.7d': 'Last 7 days', 'logs.range.7d': 'Last 7 days',
@@ -369,7 +371,7 @@ var TRANSLATIONS = {
'settings.profile.email': 'Email', 'settings.profile.email': 'Email',
'settings.profile.role': 'Role', 'settings.profile.role': 'Role',
'settings.page.title': 'Account settings', 'settings.page.title': 'Account settings',
'settings.page.subtitle': 'Manage the live profile, password and workspace settings available in this build. Planned capabilities stay visible, but they do not pretend to be wired yet.', 'settings.page.subtitle': 'Manage your profile, password, and workspace settings.',
'settings.ws.slug_hint': 'Used in API endpoints and across the console. Only lowercase letters, numbers and hyphens.', 'settings.ws.slug_hint': 'Used in API endpoints and across the console. Only lowercase letters, numbers and hyphens.',
'settings.ws.description_optional': 'Description (optional)', 'settings.ws.description_optional': 'Description (optional)',
'settings.ws.description_placeholder': 'What does this workspace do?', 'settings.ws.description_placeholder': 'What does this workspace do?',
@@ -405,8 +407,8 @@ var TRANSLATIONS = {
'settings.session.current_workspace': 'current workspace', 'settings.session.current_workspace': 'current workspace',
'settings.session.summary': '{email} · {role} in {workspace}. Use Log out to revoke the current browser session.', 'settings.session.summary': '{email} · {role} in {workspace}. Use Log out to revoke the current browser session.',
'settings.notifications.title': 'Notification preferences', 'settings.notifications.title': 'Notification preferences',
'settings.notifications.not_ready_title': 'Notifications are not wired yet.', 'settings.notifications.not_ready_title': 'Notifications are coming later.',
'settings.notifications.not_ready_body': 'Alert routing and per-user preferences are planned, but this build does not persist or deliver notification settings.', 'settings.notifications.not_ready_body': 'Notification routing and per-user preferences will be added later.',
'settings.planned_badge': 'Planned', 'settings.planned_badge': 'Planned',
'settings.notifications.spike_title': 'Operation error spike', 'settings.notifications.spike_title': 'Operation error spike',
'settings.notifications.spike_body': 'Alert when error rate exceeds a rolling threshold for a published tool.', 'settings.notifications.spike_body': 'Alert when error rate exceeds a rolling threshold for a published tool.',
@@ -446,7 +448,8 @@ var TRANSLATIONS = {
'workspace_setup.members.invite_sent': '✓ Invite sent.', 'workspace_setup.members.invite_sent': '✓ Invite sent.',
'workspace_setup.members.you': '(you)', 'workspace_setup.members.you': '(you)',
'workspace_setup.members.today': 'Today', 'workspace_setup.members.today': 'Today',
'workspace_setup.members.days_ago': '{count} days ago', 'workspace_setup.members.days_ago.one': '{count} day ago',
'workspace_setup.members.days_ago.other': '{count} days ago',
'workspace_setup.members.week_ago': '1 week ago', 'workspace_setup.members.week_ago': '1 week ago',
'workspace_setup.members.never': 'Never', 'workspace_setup.members.never': 'Never',
'workspace_setup.members.pending': 'Pending invites', 'workspace_setup.members.pending': 'Pending invites',
@@ -477,8 +480,8 @@ var TRANSLATIONS = {
'workspace_setup.role.admin': 'Admin', 'workspace_setup.role.admin': 'Admin',
'workspace_setup.role.operator': 'Developer', 'workspace_setup.role.operator': 'Developer',
'workspace_setup.role.viewer': 'Viewer', 'workspace_setup.role.viewer': 'Viewer',
'workspace_setup.member_count': '{count} members', 'workspace_setup.member_count.one': '{count} member',
'workspace_setup.member_count_one': '1 member', 'workspace_setup.member_count.other': '{count} members',
'workspace_setup.saving_create': 'Creating…', 'workspace_setup.saving_create': 'Creating…',
'workspace_setup.saving_update': 'Saving…', 'workspace_setup.saving_update': 'Saving…',
'workspace_setup.saved': 'Saved ✓', 'workspace_setup.saved': 'Saved ✓',
@@ -572,7 +575,7 @@ var TRANSLATIONS = {
'wizard.step2.auth_mode.existing': 'Use existing auth profile', 'wizard.step2.auth_mode.existing': 'Use existing auth profile',
'wizard.step2.auth_mode.create': 'Create auth profile now', 'wizard.step2.auth_mode.create': 'Create auth profile now',
'wizard.step2.auth_profile': 'Auth profile', 'wizard.step2.auth_profile': 'Auth profile',
'wizard.step2.auth_profile_hint': 'Selected profile will be resolved at runtime and attached through `execution_config.auth_profile_ref`.', 'wizard.step2.auth_profile_hint': 'The selected auth profile is applied automatically when the tool runs.',
'wizard.step2.auth_profile_empty': 'No auth profiles yet. Create one below or on the Secrets page.', 'wizard.step2.auth_profile_empty': 'No auth profiles yet. Create one below or on the Secrets page.',
'wizard.step2.create_profile_title': 'Create auth profile', 'wizard.step2.create_profile_title': 'Create auth profile',
'wizard.step2.create_profile_subtitle': 'Create a reusable profile backed by encrypted workspace secrets.', 'wizard.step2.create_profile_subtitle': 'Create a reusable profile backed by encrypted workspace secrets.',
@@ -645,7 +648,8 @@ var TRANSLATIONS = {
'wizard.step3.grpc.paste_content': 'Paste content', 'wizard.step3.grpc.paste_content': 'Paste content',
'wizard.step3.grpc.parse_proto': 'Parse proto', 'wizard.step3.grpc.parse_proto': 'Parse proto',
'wizard.step3.grpc.streaming_excluded': 'Streaming methods excluded', 'wizard.step3.grpc.streaming_excluded': 'Streaming methods excluded',
'wizard.step3.grpc.streaming_hidden': '{count} streaming method(s) found and hidden — not compatible with MCP tool calls.', 'wizard.step3.grpc.streaming_hidden.one': 'Found and hid {count} streaming method — it is not compatible with MCP tool calls.',
'wizard.step3.grpc.streaming_hidden.other': 'Found and hid {count} streaming methods — they are not compatible with MCP tool calls.',
'wizard.step3.grpc.select_method': 'Select method', 'wizard.step3.grpc.select_method': 'Select method',
'wizard.step3.grpc.discovery_live_title': 'Descriptor-driven discovery', 'wizard.step3.grpc.discovery_live_title': 'Descriptor-driven discovery',
'wizard.step3.grpc.discovery_live_subtitle': 'Use the saved draft and a descriptor set to discover services through the backend', 'wizard.step3.grpc.discovery_live_subtitle': 'Use the saved draft and a descriptor set to discover services through the backend',
@@ -832,9 +836,12 @@ var TRANSLATIONS = {
'wizard.test.completed_body': 'The current draft executed successfully against the upstream.', 'wizard.test.completed_body': 'The current draft executed successfully against the upstream.',
'wizard.test.failed_body': 'The draft executed, but the backend returned validation or runtime errors.', 'wizard.test.failed_body': 'The draft executed, but the backend returned validation or runtime errors.',
'wizard.test.window_completed': 'Window test completed', 'wizard.test.window_completed': 'Window test completed',
'wizard.test.window_completed_body': 'Window complete: {window_complete}. Truncated: {truncated}. Has more: {has_more}.', '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.session_started': 'Session test started', 'wizard.test.session_started': 'Session test started',
'wizard.test.session_started_body': 'Session {session_id} was created. Poll after {poll_after_ms} ms from the stream sessions view.', 'wizard.test.session_started_body': 'Session {session_id} was created. Check Stream sessions in a few seconds.',
'wizard.test.async_job_started': 'Async job test started', 'wizard.test.async_job_started': 'Async job test started',
'wizard.test.async_job_started_body': 'Async job {job_id} was created. Track status and result from the async jobs view.', 'wizard.test.async_job_started_body': 'Async job {job_id} was created. Track status and result from the async jobs view.',
'wizard.test.no_response': 'No test response yet', 'wizard.test.no_response': 'No test response yet',
@@ -867,7 +874,11 @@ var TRANSLATIONS = {
'wizard.grpc.no_methods': 'No unary RPC methods found.', 'wizard.grpc.no_methods': 'No unary RPC methods found.',
'wizard.grpc.fields_unresolved': 'Fields not resolved.', 'wizard.grpc.fields_unresolved': 'Fields not resolved.',
'wizard.grpc.fields_unresolved_body': 'Import or external types are not expanded client-side.', 'wizard.grpc.fields_unresolved_body': 'Import or external types are not expanded client-side.',
'wizard.grpc.services_found': '{services} services · {methods} unary methods found', 'wizard.grpc.services_found': 'Found {services_label} and {methods_label}.',
'wizard.grpc.services_label.one': '{count} service',
'wizard.grpc.services_label.other': '{count} services',
'wizard.grpc.methods_label.one': '{count} unary method',
'wizard.grpc.methods_label.other': '{count} unary methods',
'wizard.grpc.services_discovered': '{services} services · {methods} unary methods discovered', 'wizard.grpc.services_discovered': '{services} services · {methods} unary methods discovered',
'wizard.grpc.services_none': 'No unary methods discovered yet', 'wizard.grpc.services_none': 'No unary methods discovered yet',
@@ -938,7 +949,7 @@ var TRANSLATIONS = {
'agents.drawer.ops_no_match': 'No operations match "{query}"', 'agents.drawer.ops_no_match': 'No operations match "{query}"',
'agents.drawer.ops_selected': '{count} operations selected', 'agents.drawer.ops_selected': '{count} operations selected',
'agents.drawer.clear_all': 'Clear all', 'agents.drawer.clear_all': 'Clear all',
'agents.drawer.recommendation': "You've selected {count} tools. LLMs perform best with <15 tools per agent. Consider splitting into separate agents by use-case.", '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.cancel': 'Cancel',
'agents.drawer.create': 'Create agent', 'agents.drawer.create': 'Create agent',
'agents.drawer.save': 'Save changes', 'agents.drawer.save': 'Save changes',
@@ -989,7 +1000,7 @@ var TRANSLATIONS = {
'login.request_prefix': "Don't have an account?", 'login.request_prefix': "Don't have an account?",
'login.request_link': 'Request access', 'login.request_link': 'Request access',
'login.planned_badge': 'Planned', 'login.planned_badge': 'Planned',
'login.password_only': 'This build supports password sign-in only. Password reset, Google SSO and self-service access requests are not wired yet.', 'login.password_only': 'Sign in with email and password.',
'login.error.required': 'Please enter your email and password.', 'login.error.required': 'Please enter your email and password.',
'login.error.invalid': 'Invalid email or password. Please try again.', 'login.error.invalid': 'Invalid email or password. Please try again.',
'login.error.generic': 'Unable to sign in right now. Please try again.', 'login.error.generic': 'Unable to sign in right now. Please try again.',
@@ -1095,7 +1106,7 @@ var TRANSLATIONS = {
'apikeys.th.created':'СОЗДАН', 'apikeys.th.created':'СОЗДАН',
'apikeys.th.last': 'ПОСЛЕДНЕЕ ИСПОЛЬЗОВАНИЕ', 'apikeys.th.last': 'ПОСЛЕДНЕЕ ИСПОЛЬЗОВАНИЕ',
'apikeys.callout.title': 'Ключ показывается только один раз.', 'apikeys.callout.title': 'Ключ показывается только один раз.',
'apikeys.callout.body': 'Скопируйте и сохраните его сразу после создания — Crank хранит только хэш. `Последнее использование` обновляется после успешной MCP-аутентификации.', 'apikeys.callout.body': 'Скопируйте и сохраните ключ сразу после создания — Crank хранит только хэш. Поле «Последнее использование» обновляется после успешного MCP-вызова.',
'apikeys.active.title': 'Активные ключи', 'apikeys.active.title': 'Активные ключи',
'apikeys.active.subtitle': '{active} активных · {revoked} отозванных', 'apikeys.active.subtitle': '{active} активных · {revoked} отозванных',
'apikeys.search': 'Фильтр ключей…', 'apikeys.search': 'Фильтр ключей…',
@@ -1154,7 +1165,7 @@ var TRANSLATIONS = {
'secrets.subtitle': 'Зашифрованные upstream-учетные данные для bearer token, basic auth и API key, которые используют auth profiles.', 'secrets.subtitle': 'Зашифрованные upstream-учетные данные для bearer token, basic auth и API key, которые используют auth profiles.',
'secrets.new': 'Создать секрет', 'secrets.new': 'Создать секрет',
'secrets.callout.title': 'Plaintext секрета доступен только на запись.', 'secrets.callout.title': 'Plaintext секрета доступен только на запись.',
'secrets.callout.body': 'Crank шифрует secret values мастер-ключом воркспейса. После создания или ротации API возвращает только metadata, поэтому эта страница сфокусирована на lifecycle и usage references.', 'secrets.callout.body': 'Crank шифрует значения секретов мастер-ключом воркспейса. После создания или ротации API возвращает только метаданные, поэтому на этой странице доступны управление секретами и просмотр их использования.',
'secrets.active.title': 'Секреты воркспейса', 'secrets.active.title': 'Секреты воркспейса',
'secrets.active.subtitle': '{active} активных · {total} всего', 'secrets.active.subtitle': '{active} активных · {total} всего',
'secrets.search': 'Фильтр секретов…', 'secrets.search': 'Фильтр секретов…',
@@ -1171,7 +1182,10 @@ var TRANSLATIONS = {
'secrets.status.active': 'Активен', 'secrets.status.active': 'Активен',
'secrets.status.disabled': 'Отключен', 'secrets.status.disabled': 'Отключен',
'secrets.last_used.never': 'Никогда', 'secrets.last_used.never': 'Никогда',
'secrets.used_by_count': '{count} профилей', '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.loading': 'Загрузка…',
'secrets.empty.none': 'Секретов пока нет', 'secrets.empty.none': 'Секретов пока нет',
'secrets.empty.search': 'Нет секретов по текущему поиску', 'secrets.empty.search': 'Нет секретов по текущему поиску',
@@ -1213,19 +1227,22 @@ var TRANSLATIONS = {
'secrets.hint.token': 'Лучший вариант для bearer tokens и простых API key values.', 'secrets.hint.token': 'Лучший вариант для bearer tokens и простых API key values.',
'secrets.hint.username_password': 'Хранит логин и пароль как один зашифрованный payload.', 'secrets.hint.username_password': 'Хранит логин и пароль как один зашифрованный payload.',
'secrets.hint.header': 'Хранит сырое значение заголовка. Имя заголовка задается в auth profile.', 'secrets.hint.header': 'Хранит сырое значение заголовка. Имя заголовка задается в auth profile.',
'secrets.hint.generic': 'Хранит произвольный JSON для advanced future integrations.', 'secrets.hint.generic': 'Хранит произвольный JSON для нестандартных интеграций, которым нужен структурированный секрет.',
'secrets.validation.name_required': 'Нужно указать имя секрета.', 'secrets.validation.name_required': 'Нужно указать имя секрета.',
'secrets.validation.value_required': 'Нужно указать значение секрета.', 'secrets.validation.value_required': 'Нужно указать значение секрета.',
'secrets.validation.username_password_required': 'Нужно указать и логин, и пароль.', 'secrets.validation.username_password_required': 'Нужно указать и логин, и пароль.',
'secrets.validation.json_invalid': 'Generic secret payload должен быть валидным JSON.', 'secrets.validation.json_invalid': 'Generic secret payload должен быть валидным JSON.',
'secrets.profiles.title': 'Ссылки auth profiles', 'secrets.profiles.title': 'Ссылки auth profiles',
'secrets.profiles.subtitle': 'Профили резолвят секреты в runtime перед upstream execution.', 'secrets.profiles.subtitle': 'Профили резолвят секреты в runtime перед upstream execution.',
'secrets.profiles.subtitle_count': '{count} auth profiles', 'secrets.profiles.subtitle_count.one': '{count} профиль авторизации',
'secrets.profiles.subtitle_count.few': '{count} профиля авторизации',
'secrets.profiles.subtitle_count.many': '{count} профилей авторизации',
'secrets.profiles.subtitle_count.other': '{count} профиля авторизации',
'secrets.profiles.error_title': 'Не удалось загрузить ссылки auth profiles', 'secrets.profiles.error_title': 'Не удалось загрузить ссылки auth profiles',
'secrets.profiles.loading_title': 'Загрузка ссылок auth profiles…', 'secrets.profiles.loading_title': 'Загрузка ссылок auth profiles…',
'secrets.profiles.loading_body': 'Получаем auth profiles для текущего воркспейса.', 'secrets.profiles.loading_body': 'Получаем auth profiles для текущего воркспейса.',
'secrets.profiles.empty_title': 'Auth profiles пока нет', 'secrets.profiles.empty_title': 'Auth profiles пока нет',
'secrets.profiles.empty_body': 'В следующем срезе появится UI для привязки секретов к upstream authentication через auth selector.', 'secrets.profiles.empty_body': 'Создайте auth profile в мастере при настройке операции.',
'secrets.profiles.references': 'Связанные секреты', 'secrets.profiles.references': 'Связанные секреты',
'secrets.profiles.reference_count': '{count} ссылок', 'secrets.profiles.reference_count': '{count} ссылок',
'secrets.profiles.meta.created': 'Создан', 'secrets.profiles.meta.created': 'Создан',
@@ -1253,7 +1270,7 @@ var TRANSLATIONS = {
'logs.paused': 'Пауза', 'logs.paused': 'Пауза',
'logs.refresh': 'Обновить', 'logs.refresh': 'Обновить',
'logs.range.30m': 'Последние 30 мин', 'logs.range.30m': 'Последние 30 мин',
'logs.range.1h': 'Последний 1 час', 'logs.range.1h': 'Последний час',
'logs.range.6h': 'Последние 6 часов', 'logs.range.6h': 'Последние 6 часов',
'logs.range.24h': 'Последние 24 часа', 'logs.range.24h': 'Последние 24 часа',
'logs.range.7d': 'Последние 7 дней', 'logs.range.7d': 'Последние 7 дней',
@@ -1360,7 +1377,7 @@ var TRANSLATIONS = {
'settings.profile.email': 'Email', 'settings.profile.email': 'Email',
'settings.profile.role': 'Роль', 'settings.profile.role': 'Роль',
'settings.page.title': 'Настройки аккаунта', 'settings.page.title': 'Настройки аккаунта',
'settings.page.subtitle': 'Управляйте живыми настройками профиля, пароля и воркспейса, доступными в этой сборке. Запланированные возможности остаются видимыми, но не притворяются уже подключенными.', 'settings.page.subtitle': 'Управляйте профилем, паролем и настройками воркспейса.',
'settings.ws.slug_hint': 'Используется в API endpoint-ах и по всей консоли. Только строчные буквы, цифры и дефисы.', 'settings.ws.slug_hint': 'Используется в API endpoint-ах и по всей консоли. Только строчные буквы, цифры и дефисы.',
'settings.ws.description_optional': 'Описание (необязательно)', 'settings.ws.description_optional': 'Описание (необязательно)',
'settings.ws.description_placeholder': 'Чем занимается этот воркспейс?', 'settings.ws.description_placeholder': 'Чем занимается этот воркспейс?',
@@ -1397,7 +1414,7 @@ var TRANSLATIONS = {
'settings.session.summary': '{email} · {role} в {workspace}. Используйте «Выйти», чтобы отозвать текущую browser session.', 'settings.session.summary': '{email} · {role} в {workspace}. Используйте «Выйти», чтобы отозвать текущую browser session.',
'settings.notifications.title': 'Настройки уведомлений', 'settings.notifications.title': 'Настройки уведомлений',
'settings.notifications.not_ready_title': 'Уведомления пока не подключены.', 'settings.notifications.not_ready_title': 'Уведомления пока не подключены.',
'settings.notifications.not_ready_body': 'Маршрутизация алертов и настройки на пользователя запланированы, но эта сборка не сохраняет и не доставляет notification settings.', 'settings.notifications.not_ready_body': 'Маршрутизация уведомлений и персональные настройки появятся позже.',
'settings.planned_badge': 'Запланировано', 'settings.planned_badge': 'Запланировано',
'settings.notifications.spike_title': 'Всплеск ошибок операции', 'settings.notifications.spike_title': 'Всплеск ошибок операции',
'settings.notifications.spike_body': 'Алерт, когда доля ошибок превышает скользящий порог для опубликованного инструмента.', 'settings.notifications.spike_body': 'Алерт, когда доля ошибок превышает скользящий порог для опубликованного инструмента.',
@@ -1437,7 +1454,10 @@ var TRANSLATIONS = {
'workspace_setup.members.invite_sent': '✓ Приглашение отправлено.', 'workspace_setup.members.invite_sent': '✓ Приглашение отправлено.',
'workspace_setup.members.you': '(вы)', 'workspace_setup.members.you': '(вы)',
'workspace_setup.members.today': 'Сегодня', 'workspace_setup.members.today': 'Сегодня',
'workspace_setup.members.days_ago': '{count} дня назад', 'workspace_setup.members.days_ago.one': '{count} день назад',
'workspace_setup.members.days_ago.few': '{count} дня назад',
'workspace_setup.members.days_ago.many': '{count} дней назад',
'workspace_setup.members.days_ago.other': '{count} дня назад',
'workspace_setup.members.week_ago': '1 неделю назад', 'workspace_setup.members.week_ago': '1 неделю назад',
'workspace_setup.members.never': 'Никогда', 'workspace_setup.members.never': 'Никогда',
'workspace_setup.members.pending': 'Ожидающие приглашения', 'workspace_setup.members.pending': 'Ожидающие приглашения',
@@ -1456,7 +1476,7 @@ var TRANSLATIONS = {
'workspace_setup.actions.save': 'Сохранить изменения', 'workspace_setup.actions.save': 'Сохранить изменения',
'workspace_setup.create.title': 'Создать новый воркспейс', 'workspace_setup.create.title': 'Создать новый воркспейс',
'workspace_setup.create.subtitle': 'Воркспейс — это общее окружение для MCP-операций и агентов вашей команды. У каждого воркспейса свой namespace MCP endpoint-ов.', 'workspace_setup.create.subtitle': 'Воркспейс — это общее окружение для MCP-операций и агентов вашей команды. У каждого воркспейса свой namespace MCP endpoint-ов.',
'workspace_setup.create.footer': 'Вы станете Owner этого воркспейса. После создания можно пригласить дополнительных участников.', 'workspace_setup.create.footer': 'Вы станете владельцем этого воркспейса. После создания можно пригласить дополнительных участников.',
'workspace_setup.danger.title': 'Опасная зона', 'workspace_setup.danger.title': 'Опасная зона',
'workspace_setup.danger.export_title': 'Экспортировать все данные', 'workspace_setup.danger.export_title': 'Экспортировать все данные',
'workspace_setup.danger.export_body': 'Скачать JSON-снимок настроек воркспейса, участников, приглашений, операций, агентов и platform API keys.', 'workspace_setup.danger.export_body': 'Скачать JSON-снимок настроек воркспейса, участников, приглашений, операций, агентов и platform API keys.',
@@ -1468,8 +1488,10 @@ var TRANSLATIONS = {
'workspace_setup.role.admin': 'Администратор', 'workspace_setup.role.admin': 'Администратор',
'workspace_setup.role.operator': 'Разработчик', 'workspace_setup.role.operator': 'Разработчик',
'workspace_setup.role.viewer': 'Наблюдатель', 'workspace_setup.role.viewer': 'Наблюдатель',
'workspace_setup.member_count': '{count} участников', 'workspace_setup.member_count.one': '{count} участник',
'workspace_setup.member_count_one': '1 участник', 'workspace_setup.member_count.few': '{count} участника',
'workspace_setup.member_count.many': '{count} участников',
'workspace_setup.member_count.other': '{count} участника',
'workspace_setup.saving_create': 'Создание…', 'workspace_setup.saving_create': 'Создание…',
'workspace_setup.saving_update': 'Сохранение…', 'workspace_setup.saving_update': 'Сохранение…',
'workspace_setup.saved': 'Сохранено ✓', 'workspace_setup.saved': 'Сохранено ✓',
@@ -1527,7 +1549,7 @@ var TRANSLATIONS = {
'wizard.step1.rest_name': 'REST / HTTP', 'wizard.step1.rest_name': 'REST / HTTP',
'wizard.step1.rest_tagline': 'Стандартные HTTP-методы поверх REST endpoint-а. Самый универсальный вариант.', 'wizard.step1.rest_tagline': 'Стандартные HTTP-методы поверх REST endpoint-а. Самый универсальный вариант.',
'wizard.step1.graphql_name': 'GraphQL', 'wizard.step1.graphql_name': 'GraphQL',
'wizard.step1.graphql_tagline': 'Запросы и мутации к GraphQL-схеме. Поддерживаются query и mutation для одной фиксированной операции.', 'wizard.step1.graphql_tagline': 'Операции по GraphQL-схеме. Поддерживаются Query и Mutation для одной фиксированной операции.',
'wizard.step1.grpc_name': 'gRPC', 'wizard.step1.grpc_name': 'gRPC',
'wizard.step1.grpc_tagline': 'Высокопроизводительный бинарный RPC поверх HTTP/2 с unary и bounded streaming flow для низколатентных внутренних сервисов.', 'wizard.step1.grpc_tagline': 'Высокопроизводительный бинарный RPC поверх HTTP/2 с unary и bounded streaming flow для низколатентных внутренних сервисов.',
'wizard.step1.websocket_name': 'WebSocket', 'wizard.step1.websocket_name': 'WebSocket',
@@ -1550,6 +1572,7 @@ var TRANSLATIONS = {
'wizard.step2.change': 'Изменить', 'wizard.step2.change': 'Изменить',
'wizard.step2.auth_none': 'Без авторизации', 'wizard.step2.auth_none': 'Без авторизации',
'wizard.step2.auth_bearer': 'Bearer token', 'wizard.step2.auth_bearer': 'Bearer token',
'wizard.step2.auth_basic': 'Basic auth',
'wizard.step2.auth_apikey': 'API key', 'wizard.step2.auth_apikey': 'API key',
'wizard.step2.register_new': 'Зарегистрировать новый upstream', 'wizard.step2.register_new': 'Зарегистрировать новый upstream',
'wizard.step2.name': 'Имя', 'wizard.step2.name': 'Имя',
@@ -1562,7 +1585,7 @@ var TRANSLATIONS = {
'wizard.step2.auth_mode.existing': 'Использовать существующий auth profile', 'wizard.step2.auth_mode.existing': 'Использовать существующий auth profile',
'wizard.step2.auth_mode.create': 'Создать auth profile сейчас', 'wizard.step2.auth_mode.create': 'Создать auth profile сейчас',
'wizard.step2.auth_profile': 'Auth profile', 'wizard.step2.auth_profile': 'Auth profile',
'wizard.step2.auth_profile_hint': 'Выбранный профиль будет резолвиться в runtime и подключаться через `execution_config.auth_profile_ref`.', 'wizard.step2.auth_profile_hint': 'Выбранный профиль применяется автоматически при каждом вызове инструмента.',
'wizard.step2.auth_profile_empty': 'Auth profiles пока нет. Создайте его ниже или на странице Secrets.', 'wizard.step2.auth_profile_empty': 'Auth profiles пока нет. Создайте его ниже или на странице Secrets.',
'wizard.step2.create_profile_title': 'Создать auth profile', 'wizard.step2.create_profile_title': 'Создать auth profile',
'wizard.step2.create_profile_subtitle': 'Создайте переиспользуемый профиль на базе зашифрованных workspace secrets.', 'wizard.step2.create_profile_subtitle': 'Создайте переиспользуемый профиль на базе зашифрованных workspace secrets.',
@@ -1635,7 +1658,10 @@ var TRANSLATIONS = {
'wizard.step3.grpc.paste_content': 'Вставить содержимое', 'wizard.step3.grpc.paste_content': 'Вставить содержимое',
'wizard.step3.grpc.parse_proto': 'Разобрать proto', 'wizard.step3.grpc.parse_proto': 'Разобрать proto',
'wizard.step3.grpc.streaming_excluded': 'Стриминговые методы исключены', 'wizard.step3.grpc.streaming_excluded': 'Стриминговые методы исключены',
'wizard.step3.grpc.streaming_hidden': 'Найдено и скрыто streaming method(s): {count} — они несовместимы с MCP tool calls.', 'wizard.step3.grpc.streaming_hidden.one': 'Найден и скрыт {count} streaming method — он не поддерживается в MCP tool calls.',
'wizard.step3.grpc.streaming_hidden.few': 'Найдены и скрыты {count} streaming methods — они не поддерживаются в MCP tool calls.',
'wizard.step3.grpc.streaming_hidden.many': 'Найдено и скрыто {count} streaming methods — они не поддерживаются в MCP tool calls.',
'wizard.step3.grpc.streaming_hidden.other': 'Найдены и скрыты {count} streaming methods — они не поддерживаются в MCP tool calls.',
'wizard.step3.grpc.select_method': 'Выберите метод', 'wizard.step3.grpc.select_method': 'Выберите метод',
'wizard.step3.grpc.discovery_live_title': 'Поиск через descriptor set', 'wizard.step3.grpc.discovery_live_title': 'Поиск через descriptor set',
'wizard.step3.grpc.discovery_live_subtitle': 'Используйте сохраненный черновик и descriptor set, чтобы открыть сервисы через backend', 'wizard.step3.grpc.discovery_live_subtitle': 'Используйте сохраненный черновик и descriptor set, чтобы открыть сервисы через backend',
@@ -1822,9 +1848,12 @@ var TRANSLATIONS = {
'wizard.test.completed_body': 'Текущий черновик успешно выполнился против upstream.', 'wizard.test.completed_body': 'Текущий черновик успешно выполнился против upstream.',
'wizard.test.failed_body': 'Черновик выполнился, но backend вернул ошибки валидации или runtime.', 'wizard.test.failed_body': 'Черновик выполнился, но backend вернул ошибки валидации или runtime.',
'wizard.test.window_completed': 'Оконный тест завершен', 'wizard.test.window_completed': 'Оконный тест завершен',
'wizard.test.window_completed_body': 'Окно завершено: {window_complete}. Усечено: {truncated}. Есть продолжение: {has_more}.', 'wizard.test.window_completed_body': 'Собран ограниченный результат окна.',
'wizard.test.window_truncated_note': 'Результат был усечен.',
'wizard.test.window_more_note': 'Доступно продолжение, если перейти к streaming-представлениям.',
'wizard.test.window_incomplete_note': 'Upstream-поток завершился раньше, чем удалось собрать всё окно.',
'wizard.test.session_started': 'Session-тест запущен', 'wizard.test.session_started': 'Session-тест запущен',
'wizard.test.session_started_body': 'Создана session {session_id}. Следующий poll можно делать через {poll_after_ms} мс из представления stream sessions.', 'wizard.test.session_started_body': 'Сессия {session_id} создана. Проверьте статус в разделе Stream sessions через несколько секунд.',
'wizard.test.async_job_started': 'Async job тест запущен', 'wizard.test.async_job_started': 'Async job тест запущен',
'wizard.test.async_job_started_body': 'Создан async job {job_id}. Отслеживайте статус и результат через представление async jobs.', 'wizard.test.async_job_started_body': 'Создан async job {job_id}. Отслеживайте статус и результат через представление async jobs.',
'wizard.test.no_response': 'Тестового ответа пока нет', 'wizard.test.no_response': 'Тестового ответа пока нет',
@@ -1857,7 +1886,15 @@ var TRANSLATIONS = {
'wizard.grpc.no_methods': 'Unary RPC methods не найдены.', 'wizard.grpc.no_methods': 'Unary RPC methods не найдены.',
'wizard.grpc.fields_unresolved': 'Поля не разрешены.', 'wizard.grpc.fields_unresolved': 'Поля не разрешены.',
'wizard.grpc.fields_unresolved_body': 'Import-ы или внешние типы не разворачиваются на стороне клиента.', 'wizard.grpc.fields_unresolved_body': 'Import-ы или внешние типы не разворачиваются на стороне клиента.',
'wizard.grpc.services_found': 'Сервисов: {services} · unary методов: {methods}', 'wizard.grpc.services_found': 'Найдено {services_label} и {methods_label}.',
'wizard.grpc.services_label.one': '{count} сервис',
'wizard.grpc.services_label.few': '{count} сервиса',
'wizard.grpc.services_label.many': '{count} сервисов',
'wizard.grpc.services_label.other': '{count} сервиса',
'wizard.grpc.methods_label.one': '{count} unary-метод',
'wizard.grpc.methods_label.few': '{count} unary-метода',
'wizard.grpc.methods_label.many': '{count} unary-методов',
'wizard.grpc.methods_label.other': '{count} unary-метода',
'wizard.grpc.services_discovered': 'Сервисов открыто: {services} · unary методов: {methods}', 'wizard.grpc.services_discovered': 'Сервисов открыто: {services} · unary методов: {methods}',
'wizard.grpc.services_none': 'Unary methods пока не открыты', 'wizard.grpc.services_none': 'Unary methods пока не открыты',
@@ -1979,7 +2016,7 @@ var TRANSLATIONS = {
'login.request_prefix': 'Нет аккаунта?', 'login.request_prefix': 'Нет аккаунта?',
'login.request_link': 'Запросить доступ', 'login.request_link': 'Запросить доступ',
'login.planned_badge': 'Запланировано', 'login.planned_badge': 'Запланировано',
'login.password_only': 'В этой сборке доступен только вход по паролю. Сброс пароля, Google SSO и самостоятельный запрос доступа пока не подключены.', 'login.password_only': 'Войдите с помощью email и пароля.',
'login.error.required': 'Введите email и пароль.', 'login.error.required': 'Введите email и пароль.',
'login.error.invalid': 'Неверный email или пароль. Попробуйте еще раз.', 'login.error.invalid': 'Неверный email или пароль. Попробуйте еще раз.',
'login.error.generic': 'Сейчас не удается войти. Попробуйте еще раз.', 'login.error.generic': 'Сейчас не удается войти. Попробуйте еще раз.',
@@ -2232,6 +2269,33 @@ function tf(key, vars) {
}, t(key)); }, 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) { function hasTranslation(key) {
return t(key) !== key; return t(key) !== key;
} }
@@ -2289,6 +2353,7 @@ function localizeDemoOperation(operation) {
window.localizeDemoAgent = localizeDemoAgent; window.localizeDemoAgent = localizeDemoAgent;
window.localizeDemoOperation = localizeDemoOperation; window.localizeDemoOperation = localizeDemoOperation;
window.tPlural = tPlural;
function applyLang() { function applyLang() {
// text content // text content
+6 -4
View File
@@ -265,7 +265,7 @@ document.addEventListener('DOMContentLoaded', function () {
'<td>' + secretKindLabel(secret.kind) + '</td>', '<td>' + secretKindLabel(secret.kind) + '</td>',
'<td class="col-mono">v' + secret.current_version + '</td>', '<td class="col-mono">v' + secret.current_version + '</td>',
'<td>' + (secret.last_used_at ? formatDate(secret.last_used_at) : tKey('secrets.last_used.never')) + '</td>', '<td>' + (secret.last_used_at ? formatDate(secret.last_used_at) : tKey('secrets.last_used.never')) + '</td>',
'<td>' + tfKey('secrets.used_by_count', { count: references.length }) + '</td>', '<td>' + window.tPlural('secrets.used_by_count', references.length, { count: references.length }) + '</td>',
'<td><span class="badge ' + (secret.status === 'disabled' ? 'badge-revoked' : 'badge-active') + '">' + tKey('secrets.status.' + secret.status) + '</span></td>', '<td><span class="badge ' + (secret.status === 'disabled' ? 'badge-revoked' : 'badge-active') + '">' + tKey('secrets.status.' + secret.status) + '</span></td>',
'<td class="col-actions"></td>' '<td class="col-actions"></td>'
].join(''); ].join('');
@@ -298,9 +298,11 @@ document.addEventListener('DOMContentLoaded', function () {
secretsById[secret.id] = secret; secretsById[secret.id] = secret;
}); });
profilesSummary.textContent = tfKey('secrets.profiles.subtitle_count', { profilesSummary.textContent = window.tPlural(
count: state.profiles.length, 'secrets.profiles.subtitle_count',
}); state.profiles.length,
{ count: state.profiles.length }
);
if (state.error) { if (state.error) {
profilesList.innerHTML = '<div class="empty-state"><div class="empty-state-title">' + tKey('secrets.profiles.error_title') + '</div><div class="empty-state-text">' + state.error + '</div></div>'; profilesList.innerHTML = '<div class="empty-state"><div class="empty-state-title">' + tKey('secrets.profiles.error_title') + '</div><div class="empty-state-text">' + state.error + '</div></div>';
+27 -9
View File
@@ -1064,9 +1064,15 @@ function renderServiceResults(parsed, ids) {
var totalMethods = parsed.services.reduce(function(n, s) { return n + s.methods.length; }, 0); var totalMethods = parsed.services.reduce(function(n, s) { return n + s.methods.length; }, 0);
var summaryEl = document.getElementById(ids.summary); var summaryEl = document.getElementById(ids.summary);
if (summaryEl) { if (summaryEl) {
var servicesLabel = window.tPlural('wizard.grpc.services_label', parsed.services.length, {
count: parsed.services.length,
});
var methodsLabel = window.tPlural('wizard.grpc.methods_label', totalMethods, {
count: totalMethods,
});
summaryEl.textContent = tfKey('wizard.grpc.services_found', { summaryEl.textContent = tfKey('wizard.grpc.services_found', {
services: parsed.services.length, services_label: servicesLabel,
methods: totalMethods, methods_label: methodsLabel,
}); });
} }
@@ -1109,7 +1115,9 @@ function renderServiceResults(parsed, ids) {
function renderParsedProto(parsed) { function renderParsedProto(parsed) {
var notice = document.getElementById('proto-streaming-text'); var notice = document.getElementById('proto-streaming-text');
if (notice && parsed && parsed.streamingCount) { if (notice && parsed && parsed.streamingCount) {
notice.textContent = tfKey('wizard.step3.grpc.streaming_hidden', { count: parsed.streamingCount }); notice.textContent = window.tPlural('wizard.step3.grpc.streaming_hidden', parsed.streamingCount, {
count: parsed.streamingCount,
});
} }
renderServiceResults(parsed, { renderServiceResults(parsed, {
notice: 'proto-streaming-notice', count: 'proto-streaming-count', notice: 'proto-streaming-notice', count: 'proto-streaming-count',
@@ -1120,7 +1128,9 @@ function renderParsedProto(parsed) {
function renderReflectionResult(parsed) { function renderReflectionResult(parsed) {
var reflectText = document.querySelector('#reflect-streaming-notice .info-callout-text'); var reflectText = document.querySelector('#reflect-streaming-notice .info-callout-text');
if (reflectText && parsed && parsed.streamingCount) { if (reflectText && parsed && parsed.streamingCount) {
reflectText.textContent = tfKey('wizard.step3.grpc.streaming_hidden', { count: parsed.streamingCount }); reflectText.textContent = window.tPlural('wizard.step3.grpc.streaming_hidden', parsed.streamingCount, {
count: parsed.streamingCount,
});
} }
renderServiceResults(parsed, { renderServiceResults(parsed, {
notice: 'reflect-streaming-notice', count: 'reflect-streaming-count', notice: 'reflect-streaming-notice', count: 'reflect-streaming-count',
@@ -2572,13 +2582,21 @@ function describeWizardTestResult(result) {
} }
if (result.window) { if (result.window) {
var bodyParts = [
tKey('wizard.test.window_completed_body')
];
if (result.window.truncated) {
bodyParts.push(tKey('wizard.test.window_truncated_note'));
}
if (result.window.has_more) {
bodyParts.push(tKey('wizard.test.window_more_note'));
}
if (!result.window.window_complete) {
bodyParts.push(tKey('wizard.test.window_incomplete_note'));
}
return { return {
title: result.ok ? tKey('wizard.test.window_completed') : tKey('wizard.test.failed'), title: result.ok ? tKey('wizard.test.window_completed') : tKey('wizard.test.failed'),
body: tfKey('wizard.test.window_completed_body', { body: bodyParts.join(' '),
truncated: result.window.truncated ? tKey('wizard.boolean.yes') : tKey('wizard.boolean.no'),
has_more: result.window.has_more ? tKey('wizard.boolean.yes') : tKey('wizard.boolean.no'),
window_complete: result.window.window_complete ? tKey('wizard.boolean.yes') : tKey('wizard.boolean.no')
}),
isError: !result.ok, isError: !result.ok,
}; };
} }
+5 -3
View File
@@ -149,9 +149,11 @@ function renderMembers() {
workspaceFormState.memberships.forEach(function (membership) { workspaceFormState.memberships.forEach(function (membership) {
addMemberRow(container, membership); addMemberRow(container, membership);
}); });
label.textContent = workspaceFormState.memberships.length === 1 label.textContent = window.tPlural(
? tKey('workspace_setup.member_count_one') 'workspace_setup.member_count',
: tfKey('workspace_setup.member_count', { count: workspaceFormState.memberships.length }); workspaceFormState.memberships.length,
{ count: workspaceFormState.memberships.length }
);
} }
function renderInvitations() { function renderInvitations() {