-
Notifications
You must be signed in to change notification settings - Fork 436
fix(i18n): map expected failure codes instead of raw messages #4641
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one | ||
| * or more contributor license agreements. See the NOTICE file | ||
| * distributed with this work for additional information | ||
| * regarding copyright ownership. The ASF licenses this file | ||
| * to you under the Apache License, Version 2.0 (the | ||
| * "License"); you may not use this file except in compliance | ||
| * with the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, | ||
| * software distributed under the License is distributed on an | ||
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| * KIND, either express or implied. See the License for the | ||
| * specific language governing permissions and limitations | ||
| * under the License. | ||
| */ | ||
|
|
||
| import assert from 'node:assert/strict'; | ||
| import { readFileSync } from 'node:fs'; | ||
| import { test } from 'node:test'; | ||
| import { | ||
| getProviderSettingsCopy, | ||
| providerPanelActionErrorMessage, | ||
| } from '../../renderer/features/connection-settings/index.js'; | ||
| import { | ||
| getSettingsProjectsCopy, | ||
| runtimeHostManagementErrorMessage, | ||
| } from '../../renderer/locales/settings-projects-copy.js'; | ||
|
|
||
| test('Runtime Host management codes render per locale and unknown codes fall back', () => { | ||
| const rendered = (code: string) => ({ | ||
| zh: runtimeHostManagementErrorMessage(code, 'zh'), | ||
| en: runtimeHostManagementErrorMessage(code, 'en'), | ||
| }); | ||
| assert.deepEqual(rendered('active_tasks'), { | ||
| zh: 'Runtime Host 正在执行任务,请稍后再试', | ||
| en: 'Runtime Host still owns active work. Try again later.', | ||
| }); | ||
| assert.deepEqual(rendered('linger_disabled'), { | ||
| zh: '请先为当前用户启用 systemd linger,服务才能在登出后继续运行', | ||
| en: 'Enable systemd linger for this user so the service keeps running after logout.', | ||
| }); | ||
| assert.deepEqual(rendered('package_integrity_mismatch'), { | ||
| zh: '更新包校验失败', | ||
| en: 'The update package failed its integrity check.', | ||
| }); | ||
| // Commit-outcome-unknown codes fold into the generic log-pointer (managed-deployment.ts, update-policy-store.ts). | ||
| const unknownFallback = { | ||
| zh: '请查看服务日志了解详情', | ||
| en: 'Check the service logs for details.', | ||
| }; | ||
| for (const code of [ | ||
| 'deployment_commit_unknown', | ||
| 'update_policy_commit_outcome_unknown', | ||
| 'some_future_code', | ||
| ]) { | ||
| assert.deepEqual(rendered(code), unknownFallback); | ||
| } | ||
| }); | ||
|
|
||
| function extractErrorCodes(source: string, className: string): string[] { | ||
| const classIndex = source.indexOf(`class ${className}`); | ||
| assert.ok(classIndex !== -1, `expected ${className} in CLI source`); | ||
| const classBlock = source.slice(classIndex); | ||
| const codeIndex = classBlock.indexOf('readonly code:'); | ||
| const messageIndex = classBlock.indexOf('message: string'); | ||
| assert.ok(codeIndex !== -1 && messageIndex !== -1 && codeIndex < messageIndex); | ||
| const codes = classBlock | ||
| .slice(codeIndex, messageIndex) | ||
| .match(/'([a-z_]+)'/gu) | ||
| ?.map((literal) => literal.slice(1, -1)); | ||
| assert.ok(codes && codes.length > 0, `expected the ${className} code union`); | ||
| return codes; | ||
| } | ||
|
|
||
| function readCliSource(fileName: string): string { | ||
| return readFileSync( | ||
| new URL(`../../../../../packages/cli/src/${fileName}`, import.meta.url), | ||
| 'utf8', | ||
| ); | ||
| } | ||
|
|
||
| // [source file, error class] pairs presented distinctly. The two inline codes | ||
| // below have no error class (reconciliation, update-command). | ||
| const CLI_ERROR_SOURCES = [ | ||
| ['runtime-host-service-manager.ts', 'RuntimeHostServiceManagerError'], | ||
| ['runtime-host-registry-update.ts', 'RuntimeHostUpdateDiscoveryError'], | ||
| ['runtime-host-update-package.ts', 'RuntimeHostUpdatePackageError'], | ||
| ['runtime-host-update-policy-store.ts', 'RuntimeHostUpdatePolicyError'], | ||
| ] as const; | ||
|
|
||
| test('presenter maps every management code the CLI can emit', () => { | ||
| const extracted = CLI_ERROR_SOURCES.flatMap(([file, className]) => | ||
| extractErrorCodes(readCliSource(file), className), | ||
| ); | ||
| const codes = [...new Set([...extracted, 'update_policy_changed', 'update_not_admitted'])]; | ||
| assert.ok(codes.length >= 20, `expected the CLI code set, got ${codes.length}`); | ||
| const presented = Object.keys(getSettingsProjectsCopy('en').runtimeHost.managementError); | ||
| assert.deepEqual(codes.filter((code) => !presented.includes(code)), [ | ||
| 'update_policy_commit_outcome_unknown', | ||
| ]); | ||
| // Deployment-internal codes (managed-deployment.ts, …) intentionally fall back to unknown. | ||
| }); | ||
|
|
||
| test('provider action errors never echo a raw Chinese message', () => { | ||
| const error = new Error('连接失败,请稍后重试'); | ||
| assert.equal( | ||
| providerPanelActionErrorMessage(error, 'zh'), | ||
| getProviderSettingsCopy('zh').shared.actionFallback, | ||
| ); | ||
| assert.equal( | ||
| providerPanelActionErrorMessage(error, 'en'), | ||
| getProviderSettingsCopy('en').shared.actionFallback, | ||
| ); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,35 @@ | |
|
|
||
| import type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; | ||
|
|
||
| // The operator CLI keeps code an open string for version skew; this is the subset Desktop presents distinctly. | ||
| export type RuntimeHostManagementErrorCode = | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: this re-declares a code list the producers already own, so there are now two spellings of it and no compiler link between them. The four CLI error classes keep their unions inline in the constructor (packages/cli/src/runtime-host-service-manager.ts:287), and the only thing tying the two together is extractErrorCodes() scraping that source with a regex. A rename on the CLI side fails the desktop suite with an opaque assertion, and if either anchor moves (
|
||
| | 'active_tasks' | ||
| | 'not_installed' | ||
| | 'unsupported_platform' | ||
| | 'service_manager_unavailable' | ||
| | 'linger_disabled' | ||
| | 'invalid_config' | ||
| | 'invalid_launch' | ||
| | 'target_mismatch' | ||
| | 'configuration_changed' | ||
| | 'configuration_incomplete' | ||
| | 'retirement_failed' | ||
| | 'update_requires_retirement' | ||
| | 'update_incomplete' | ||
| | 'service_manager_operation_failed' | ||
| | 'uninstall_incomplete' | ||
| | 'deployment_io_failed' | ||
| | 'target_unavailable' | ||
| | 'registry_unavailable' | ||
| | 'invalid_registry_metadata' | ||
| | 'package_download_failed' | ||
| | 'package_integrity_mismatch' | ||
| | 'invalid_package' | ||
| | 'invalid_update_policy' | ||
| | 'update_policy_write_failed' | ||
| | 'update_policy_changed' | ||
| | 'update_not_admitted'; | ||
|
|
||
| export type SettingsProjectsCopy = { | ||
| runtimeHost: { | ||
| title: string; | ||
|
|
@@ -234,6 +263,7 @@ export type SettingsProjectsCopy = { | |
| uninstallConfirm: string; | ||
| uninstallRetained(path: string): string; | ||
| managementActionFailed: string; | ||
| managementError: Record<RuntimeHostManagementErrorCode | 'unknown', string>; | ||
| managementReconnectFailed: string; | ||
| manageAccess: string; | ||
| accessTitle: string; | ||
|
|
@@ -553,6 +583,35 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { | |
| uninstallConfirm: '卸载服务', | ||
| uninstallRetained: (path: string) => `服务已卸载,数据保留在 ${path}`, | ||
| managementActionFailed: '无法管理 Runtime Host 服务', | ||
| managementError: { | ||
| active_tasks: 'Runtime Host 正在执行任务,请稍后再试', | ||
| not_installed: '此 Runtime Host 服务尚未安装', | ||
| unsupported_platform: '当前系统不支持受管理的 Runtime Host 服务', | ||
| service_manager_unavailable: '系统服务管理器(systemd、launchd 或 OpenRC)不可用', | ||
| linger_disabled: '请先为当前用户启用 systemd linger,服务才能在登出后继续运行', | ||
| invalid_config: 'Runtime Host 服务配置无效', | ||
| invalid_launch: 'Runtime Host 服务启动参数无效,请重新安装', | ||
| target_mismatch: '服务已被其他安装接管,请刷新后重试', | ||
| configuration_changed: '服务配置已在别处修改,请刷新后重试', | ||
| configuration_incomplete: '服务配置不完整,请重新安装', | ||
| retirement_failed: '无法安全停止当前 Runtime Host', | ||
| update_requires_retirement: '更新前需要先停止当前 Runtime Host', | ||
| update_incomplete: '更新未完成,请查看服务日志', | ||
| service_manager_operation_failed: '系统服务管理器操作失败,请查看服务日志', | ||
| uninstall_incomplete: '卸载未完成,请重试', | ||
| deployment_io_failed: '无法写入 Runtime Host 部署文件', | ||
| target_unavailable: '找不到所选版本', | ||
| registry_unavailable: '无法连接更新源,请检查网络', | ||
| invalid_registry_metadata: '更新源返回了无效的版本信息', | ||
| package_download_failed: '更新包下载失败', | ||
| package_integrity_mismatch: '更新包校验失败', | ||
| invalid_package: '更新包无效', | ||
| invalid_update_policy: '更新策略无效', | ||
| update_policy_write_failed: '无法保存更新策略', | ||
| update_policy_changed: '更新策略已变化,请刷新后重试', | ||
| update_not_admitted: '当前版本不允许此更新', | ||
| unknown: '请查看服务日志了解详情', | ||
| }, | ||
| managementReconnectFailed: '更改已应用,但 Desktop 未能重新连接', | ||
| manageAccess: '管理访问权限', | ||
| accessTitle: '访问权限', | ||
|
|
@@ -872,6 +931,38 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { | |
| uninstallConfirm: 'Uninstall service', | ||
| uninstallRetained: (path: string) => `Service uninstalled. Data was retained at ${path}`, | ||
| managementActionFailed: 'Unable to manage the Runtime Host service', | ||
| managementError: { | ||
| active_tasks: 'Runtime Host still owns active work. Try again later.', | ||
| not_installed: 'This Runtime Host service is not installed.', | ||
| unsupported_platform: 'Managed Runtime Host services are not supported on this platform.', | ||
| service_manager_unavailable: | ||
| 'The system service manager (systemd, launchd, or OpenRC) is unavailable.', | ||
| linger_disabled: | ||
| 'Enable systemd linger for this user so the service keeps running after logout.', | ||
| invalid_config: 'The Runtime Host service configuration is invalid.', | ||
| invalid_launch: 'The Runtime Host service launch definition is invalid. Reinstall the service.', | ||
| target_mismatch: 'Another installation now owns this service. Refresh and try again.', | ||
| configuration_changed: 'The service configuration changed elsewhere. Refresh and try again.', | ||
| configuration_incomplete: 'The service configuration is incomplete. Reinstall the service.', | ||
| retirement_failed: 'The current Runtime Host could not be stopped safely.', | ||
| update_requires_retirement: 'Stop the current Runtime Host before updating.', | ||
| update_incomplete: 'The update did not complete. Check the service logs.', | ||
| service_manager_operation_failed: | ||
| 'The system service manager operation failed. Check the service logs.', | ||
| uninstall_incomplete: 'The uninstall did not complete. Try again.', | ||
| deployment_io_failed: 'Runtime Host deployment files could not be written.', | ||
| target_unavailable: 'The selected version is unavailable.', | ||
| registry_unavailable: 'The update registry is unreachable. Check the network.', | ||
| invalid_registry_metadata: 'The update registry returned invalid version metadata.', | ||
| package_download_failed: 'The update package could not be downloaded.', | ||
| package_integrity_mismatch: 'The update package failed its integrity check.', | ||
| invalid_package: 'The update package is invalid.', | ||
| invalid_update_policy: 'The update policy is invalid.', | ||
| update_policy_write_failed: 'The update policy could not be saved.', | ||
| update_policy_changed: 'The update policy changed. Refresh and try again.', | ||
| update_not_admitted: 'This update is not permitted for the installed version.', | ||
| unknown: 'Check the service logs for details.', | ||
| }, | ||
| managementReconnectFailed: 'Change applied, but Desktop could not reconnect', | ||
| manageAccess: 'Manage access', | ||
| accessTitle: 'Access', | ||
|
|
@@ -948,3 +1039,8 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { | |
| export function getSettingsProjectsCopy(locale: UiLocale): SettingsProjectsCopy { | ||
| return SETTINGS_PROJECTS_COPY_BY_LOCALE[locale]; | ||
| } | ||
|
|
||
| export function runtimeHostManagementErrorMessage(code: string, locale: UiLocale): string { | ||
| const messages = getSettingsProjectsCopy(locale).runtimeHost.managementError; | ||
| return (messages as Record<string, string>)[code] ?? messages.unknown; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: this |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -48,7 +48,10 @@ import type { | |
| DesktopRuntimeHostUpdateReconciliationOutcome, | ||
| DesktopRuntimeHostUpdateReconciliationResponse, | ||
| } from '../../preload/bridge-contract.js'; | ||
| import { getSettingsProjectsCopy } from '../locales/settings-projects-copy.js'; | ||
| import { | ||
| getSettingsProjectsCopy, | ||
| runtimeHostManagementErrorMessage, | ||
| } from '../locales/settings-projects-copy.js'; | ||
| import { | ||
| canonicalProjectDirectoryRoots, | ||
| projectDirectoryRootsValid, | ||
|
|
@@ -188,7 +191,9 @@ export function RuntimeHostManagementDialog(props: { | |
| reconcileDirectoryPolicy(response.service); | ||
| shouldLoadUpdatePolicy = response.service.state !== 'not_installed'; | ||
| } | ||
| else if (response.kind === 'error') setError(response.error.message); | ||
| else if (response.kind === 'error') { | ||
| setError(runtimeHostManagementErrorMessage(response.error.code, locale)); | ||
| } | ||
| else setUninstalledRoot(response.retainedStateRoot); | ||
| } catch (failure) { | ||
| if (!disposed) setError(settingsActionErrorMessage(failure, locale)); | ||
|
|
@@ -229,6 +234,12 @@ export function RuntimeHostManagementDialog(props: { | |
| if (logs) logs.scrollTop = logs.scrollHeight; | ||
| }, [result]); | ||
|
|
||
| function reportManagementError(code: string): string { | ||
| const message = runtimeHostManagementErrorMessage(code, locale); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: |
||
| toast.error(copy.managementActionFailed, message); | ||
| return message; | ||
| } | ||
|
|
||
| async function run( | ||
| action: DesktopRuntimeHostManagementAction, | ||
| allowInterruptActiveTasks = false, | ||
|
|
@@ -256,8 +267,7 @@ export function RuntimeHostManagementDialog(props: { | |
| return; | ||
| } | ||
| setUpdatePolicy(undefined); | ||
| setError(response.error.message); | ||
| toast.error(copy.managementActionFailed, response.error.message); | ||
| setError(reportManagementError(response.error.code)); | ||
| return; | ||
| } | ||
| if (response.kind === 'uninstalled') { | ||
|
|
@@ -375,8 +385,7 @@ export function RuntimeHostManagementDialog(props: { | |
| ); | ||
| if (response.kind === 'error') { | ||
| setUpdatePolicy(undefined); | ||
| setError(response.error.message); | ||
| toast.error(copy.managementActionFailed, response.error.message); | ||
| setError(reportManagementError(response.error.code)); | ||
| return; | ||
| } | ||
| if (response.kind === 'uninstalled') { | ||
|
|
@@ -461,8 +470,7 @@ export function RuntimeHostManagementDialog(props: { | |
| allowInterruptActiveTasks, | ||
| ); | ||
| if (response.kind === 'error') { | ||
| setError(response.error.message); | ||
| toast.error(copy.managementActionFailed, response.error.message); | ||
| setError(reportManagementError(response.error.code)); | ||
| return; | ||
| } | ||
| if (response.kind === 'uninstalled' || response.action !== 'configure') { | ||
|
|
@@ -545,8 +553,7 @@ export function RuntimeHostManagementDialog(props: { | |
| const response = await window.maka.runtimeHostManagement.reconcileUpdate(target.id); | ||
| if (response.kind === 'error') { | ||
| setUpdatePolicy(undefined); | ||
| setUpdatePolicyError(response.error.message); | ||
| toast.error(copy.managementActionFailed, response.error.message); | ||
| setUpdatePolicyError(reportManagementError(response.error.code)); | ||
| return; | ||
| } | ||
| setLastUpdateOutcome(response.reconciliation); | ||
|
|
@@ -584,12 +591,13 @@ export function RuntimeHostManagementDialog(props: { | |
| } | ||
|
|
||
| function applyReconnectWarning( | ||
| reconnectError: { readonly message: string } | undefined, | ||
| reconnectError: { readonly code: string; readonly message: string } | undefined, | ||
| ): void { | ||
| setReconnectWarning(reconnectError?.message); | ||
| if (reconnectError) { | ||
| toast.warning(copy.managementReconnectFailed, reconnectError.message); | ||
| } | ||
| setReconnectWarning(reconnectError ? copy.managementReconnectFailed : undefined); | ||
| if (!reconnectError) return; | ||
| // Raw operator detail goes to diagnostics only; users see the catalog line. | ||
| console.error('[runtime-host] reconnect failed', reconnectError); | ||
| toast.warning(copy.managementReconnectFailed); | ||
| } | ||
|
|
||
| async function revokeCredential(): Promise<void> { | ||
|
|
@@ -669,8 +677,7 @@ export function RuntimeHostManagementDialog(props: { | |
| {reconnectWarning ? ( | ||
| <Banner | ||
| status="warning" | ||
| title={copy.managementReconnectFailed} | ||
| description={reconnectWarning} | ||
| title={reconnectWarning} | ||
| /> | ||
| ) : null} | ||
| {confirmation?.kind === 'configureDirectories' ? ( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: this is the exact pattern the PR title targets, left in the file the PR edits. Main throws
new Error('Unable to delete Connection: connection_stale')at runtime-host-connections-ipc-main.ts:333 and the renderer regexes the code back out of the sentence. Same for the lastTest lookup on line 38, which is keyed on lowercased message text. Not asking you to fix it here, but the body should say that this surface and artifact-pane.tsx only lost their CJK passthrough this round and are still raw-message classified, otherwise it reads as if the whole path is code-driven now.