Skip to content

Commit 08f12ee

Browse files
committed
fix(web-ui): restore trash tab auto-load, migrate trashConfig, and complete i18n coverage
- session-helpers.mjs: load trash list when switching to the trash tab (regression from trashConfig migration; list disappeared until manual refresh) - session-actions.mjs: applySessionTrashEnabledChange clears/reloads list immediately on toggle so no manual refresh is needed - session-trash.mjs: getSessionTrashViewState returns 'disabled' when off - panel-trash.html: host trashConfig card + disabled-state UI (v-if chain updated) - panel-settings.html: remove migrated trashConfig card - styles: disabled-state styling + vertical-center action buttons - locales zh/en/ja/vi/zh-tw: fill 9 referenced-but-missing keys plus legacy gaps - tests: 2 cross-cutting i18n tests; behavior-parity allows new method key - precompiled render script regenerated
1 parent 1728627 commit 08f12ee

15 files changed

Lines changed: 435 additions & 196 deletions

tests/unit/i18n-locales.test.mjs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,3 +333,51 @@ test('zh-tw fallback resolves through zh before en', () => {
333333
// For a hypothetical missing key, it would fall back to zh then en
334334
assert.strictEqual(tFallback('nonexistent.key.xyz'), 'nonexistent.key.xyz');
335335
});
336+
337+
test('all locale key sets stay aligned across the five supported languages', () => {
338+
const baselineKeys = Object.keys(DICT.zh).sort();
339+
const baselineKeySet = new Set(baselineKeys);
340+
// ja keeps a legacy unused key not present in other locales
341+
const allowedExtraKeys = Object.freeze({
342+
ja: new Set(['sessions.preview.openStandalone'])
343+
});
344+
for (const code of expectedLocales) {
345+
const localeKeys = Object.keys(DICT[code]);
346+
const missingFromLocale = baselineKeys.filter((key) => !Object.prototype.hasOwnProperty.call(DICT[code], key));
347+
const extraInLocale = localeKeys.filter((key) => !baselineKeySet.has(key));
348+
const allowedExtra = allowedExtraKeys[code] || new Set();
349+
const unexpectedExtra = extraInLocale.filter((key) => !allowedExtra.has(key));
350+
assert.deepStrictEqual(missingFromLocale, [], `${code} must define every key present in zh baseline`);
351+
assert.deepStrictEqual(unexpectedExtra, [], `${code} defines keys beyond zh baseline (${extraInLocale.join(', ')})`);
352+
}
353+
});
354+
355+
test('every t() key referenced by templates and app modules exists in all locales', () => {
356+
const partialDir = path.join(repoRoot, 'web-ui', 'partials');
357+
const moduleDir = path.join(repoRoot, 'web-ui', 'modules');
358+
const referenceKeyPattern = /\bt\('([a-zA-Z0-9]+\.[a-zA-Z0-9_.]+)'(?:\s*,\s*\{[^}]*\})?\)/g;
359+
const referencedKeys = new Set();
360+
const walk = (dir) => {
361+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
362+
const full = path.join(dir, entry.name);
363+
if (entry.isDirectory()) { walk(full); continue; }
364+
if (!/\.(?:html|mjs|js)$/.test(entry.name)) continue;
365+
const content = fs.readFileSync(full, 'utf8');
366+
let match;
367+
while ((match = referenceKeyPattern.exec(content)) !== null) {
368+
referencedKeys.add(match[1]);
369+
}
370+
}
371+
};
372+
walk(partialDir);
373+
walk(moduleDir);
374+
for (const key of referencedKeys) {
375+
for (const code of expectedLocales) {
376+
assert.strictEqual(
377+
typeof DICT[code][key],
378+
'string',
379+
`${code} should define referenced key: ${key}`
380+
);
381+
}
382+
}
383+
});

tests/unit/web-ui-behavior-parity.test.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -860,7 +860,8 @@ test('captured bundled app skeleton only exposes expected data key drift versus
860860
'getOpenclawConfigSummary',
861861
'getOpenclawQuickWorkspaceFiles',
862862
'getOpenclawStatusSummaryItems',
863-
'openOpenclawQuickWorkspaceFile'
863+
'openOpenclawQuickWorkspaceFile',
864+
'applySessionTrashEnabledChange'
864865
);
865866
const allowedMissingCurrentMethodKeys = [
866867
'convertSession',

web-ui/modules/app.methods.session-actions.mjs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,10 +308,37 @@ export function createSessionActionMethods(options = {}) {
308308

309309
setSessionTrashEnabled(value) {
310310
const enabled = this.normalizeSessionTrashEnabled(value);
311+
const changed = this.sessionTrashEnabled !== enabled;
311312
this.sessionTrashEnabled = enabled;
312313
if (typeof this.persistWebUiPreferences === 'function') {
313314
this.persistWebUiPreferences({ sessionTrashEnabled: enabled });
314315
}
316+
// 关闭/开启回收站后立即反映当前状态,无需用户手动刷新
317+
if (changed && typeof this.applySessionTrashEnabledChange === 'function') {
318+
this.applySessionTrashEnabledChange(enabled);
319+
}
320+
},
321+
322+
applySessionTrashEnabledChange(enabled) {
323+
if (typeof this.invalidateSessionTrashRequests === 'function') {
324+
this.invalidateSessionTrashRequests();
325+
}
326+
if (enabled) {
327+
// 重新开启:立即拉取最新回收站内容
328+
if (typeof this.loadSessionTrash === 'function') {
329+
void this.loadSessionTrash({ forceRefresh: true });
330+
}
331+
} else {
332+
// 关闭回收站:即时清空已展示的列表与计数,避免残留陈旧数据需手动刷新
333+
this.sessionTrashItems = [];
334+
this.sessionTrashVisibleCount = 0;
335+
this.sessionTrashTotalCount = 0;
336+
this.sessionTrashCountLoadedOnce = false;
337+
this.sessionTrashLoadedOnce = false;
338+
this.sessionTrashLastLoadFailed = false;
339+
this.sessionTrashRestoring = {};
340+
this.sessionTrashPurging = {};
341+
}
315342
},
316343

317344
setSessionTimelineStyle(style) {

web-ui/modules/app.methods.session-trash.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ export function createSessionTrashMethods(options = {}) {
8989
},
9090

9191
getSessionTrashViewState() {
92+
const trashEnabled = this.sessionTrashEnabled !== false;
93+
// 回收站已关闭:即便残留旧列表也只展示已禁用态,不展示陈旧内容
94+
if (!trashEnabled) {
95+
return 'disabled';
96+
}
9297
if (this.sessionTrashLoading && !this.sessionTrashLoadedOnce) {
9398
return 'loading';
9499
}

web-ui/modules/i18n/locales/en.mjs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ const en = Object.freeze({
4242
'common.refresh': 'Refresh',
4343
'common.refreshing': 'Refreshing...',
4444
'common.loading': 'Loading...',
45+
'common.menu': 'Menu',
46+
'common.registry': 'Registry',
4547
'common.saving': 'Saving...',
4648
'common.sending': 'Sending...',
4749
'common.scanning': 'Scanning...',
@@ -68,6 +70,7 @@ const en = Object.freeze({
6870
'common.notConfigured': 'Not configured',
6971
'common.enabled': 'Enabled',
7072
'common.disabled': 'Disabled',
73+
'common.retry': 'Retry',
7174
'cli.missing.title': '{name} CLI not installed',
7275
'cli.missing.subtitle': 'Install {name} CLI before using this page.',
7376
'cli.missing.openDocs': 'Open install guide',
@@ -89,6 +92,7 @@ const en = Object.freeze({
8992
'field.apiEndpoint': 'API endpoint',
9093
'field.apiKey': 'API key',
9194
'field.baseUrl': 'Base URL',
95+
'field.url': 'URL',
9296
'field.provider': 'Provider',
9397
'field.providerName': 'Provider name',
9498
'field.modelName': 'Model name',
@@ -459,6 +463,7 @@ const en = Object.freeze({
459463
'toast.templates.builtinNotDuplicable': 'Built-in templates cannot be duplicated',
460464
'toast.templates.builtinNotDeletable': 'Built-in templates cannot be deleted',
461465
'toast.operation.success': 'Operation successful',
466+
'toast.operation.fail': 'Operation failed',
462467
'toast.load.fail': 'Failed to load file',
463468
'toast.apply.success': 'Configuration applied',
464469
'toast.apply.fail': 'Failed to apply configuration',
@@ -915,6 +920,11 @@ const en = Object.freeze({
915920
'config.template.editFirst': 'Edit template first, then apply.',
916921
'config.template.bridgeCodexOnly': '{hint} template is editable in Codex mode only.',
917922
'config.localBridge.enabledCount': '{enabled}/{total} enabled',
923+
'config.localBridge.poolSettings': 'Pool settings',
924+
'config.localBridge.poolHint': 'Select candidate providers to join load balancing.',
925+
'config.localBridge.noProviders': 'No candidate providers available. Add a direct provider first.',
926+
'config.transformProvider.title': 'Using transform provider',
927+
'config.providerBridgeHint': '{label} bridge mode',
918928
'config.template.openEditor': 'Open template editor',
919929
'modal.configTemplate.title': 'Config template editor (manual confirm)',
920930
'modal.configTemplate.placeholder': 'Edit config.toml template here',
@@ -1319,6 +1329,8 @@ const en = Object.freeze({
13191329
'settings.claude.meta': 'Backup / import ~/.claude',
13201330
'settings.codex.title': 'Codex config',
13211331
'settings.codex.meta': 'Backup / import ~/.codex',
1332+
'settings.backup.title': 'Data backup',
1333+
'settings.backup.meta': 'Export / import Claude and Codex config',
13221334
'settings.backup.progress': 'Backing up {percent}%',
13231335
'settings.backup.oneClickClaude': 'Backup ~/.claude',
13241336
'settings.backup.importClaude': 'Import ~/.claude backup',
@@ -1384,6 +1396,12 @@ const en = Object.freeze({
13841396
'settings.trash.retentionLabel': 'Retention days',
13851397
'settings.trash.retentionUnit': 'days',
13861398
'settings.trash.retentionHint': 'Range 1-365 days, default 30. Expired entries are purged on each trash load.',
1399+
'settings.trash.count': '{count} item(s)',
1400+
'settings.trash.retentionShort': 'Kept {days} days',
1401+
'settings.trash.disabled': 'Trash is disabled',
1402+
'settings.trash.disabledHint': 'Deleted sessions are permanently removed and no longer move to trash.',
1403+
'settings.trash.clearShort': 'Clear',
1404+
'settings.trash.loadMoreItems': 'Load more (remaining {count})',
13871405

13881406
'settings.webhook.title': 'Webhook',
13891407
'settings.webhook.meta': 'Send event notifications to an external service',

web-ui/modules/i18n/locales/ja.mjs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ const ja = Object.freeze({
4343
'common.refresh': '更新',
4444
'common.refreshing': '更新中...',
4545
'common.loading': '読み込み中...',
46+
'common.menu': 'メニュー',
47+
'common.registry': 'レジストリ',
4648
'common.saving': '保存中...',
4749
'common.sending': '送信中...',
4850
'common.scanning': 'スキャン中...',
@@ -69,6 +71,7 @@ const ja = Object.freeze({
6971
'common.notConfigured': '未設定',
7072
'common.enabled': '有効',
7173
'common.disabled': '無効',
74+
'common.retry': '再試行',
7275
'cli.missing.title': '{name} CLI がインストールされていません',
7376
'cli.missing.subtitle': '{name} CLI をインストールしてからこのページをご利用ください。',
7477
'cli.missing.openDocs': 'インストールガイドを開く',
@@ -90,6 +93,7 @@ const ja = Object.freeze({
9093
'field.apiEndpoint': 'API エンドポイント',
9194
'field.apiKey': '認証キー',
9295
'field.baseUrl': 'Base URL',
96+
'field.url': 'URL',
9397
'field.provider': 'プロバイダー',
9498
'field.providerName': 'プロバイダー名',
9599
'field.modelName': 'モデル名',
@@ -465,6 +469,7 @@ const ja = Object.freeze({
465469
'toast.templates.deleteConfirm': '削除',
466470
'toast.templates.deleteCancel': 'キャンセル',
467471
'toast.operation.success': '操作が成功しました',
472+
'toast.operation.fail': '操作に失敗しました',
468473
'toast.load.fail': 'ファイルの読み込みに失敗しました',
469474
'toast.apply.success': '設定が適用されました',
470475
'toast.apply.fail': '設定の適用に失敗しました',
@@ -666,6 +671,20 @@ const ja = Object.freeze({
666671
'sessions.preview.moving': '移動中...',
667672
'sessions.preview.export': 'エクスポート',
668673
'sessions.preview.exporting': 'エクスポート中...',
674+
'sessions.preview.importNative': 'ネイティブディレクトリへインポート',
675+
'sessions.preview.importingNative': 'インポート中...',
676+
'sessions.preview.importNative.unsupported': 'この操作はサポートされていません',
677+
'sessions.preview.importNative.confirmTitle': 'ネイティブセッションファイルを上書きしますか?',
678+
'sessions.preview.importNative.confirmMessage': 'ネイティブセッションファイルが既に存在します。上書きすると、対象ツールのネイティブディレクトリ内の同名セッションが置き換えられます。',
679+
'sessions.preview.importNative.confirmText': '上書き',
680+
'sessions.preview.importNative.cancelled': 'インポートをキャンセルしました',
681+
'sessions.preview.importNative.conflict': 'ネイティブセッションは既に存在します',
682+
'sessions.preview.importNative.invalidSource': 'セッションソースが無効です',
683+
'sessions.preview.importNative.fileNotFound': 'セッションファイルが見つかりません',
684+
'sessions.preview.importNative.nativePathUnavailable': 'ネイティブセッションパスを解析できません',
685+
'sessions.preview.importNative.success': 'ネイティブディレクトリへインポートしました',
686+
'sessions.preview.importNative.failed': 'インポート失敗',
687+
'sessions.preview.importNative.failedWithReason': 'ネイティブへのインポートに失敗しました:{reason}',
669688
'sessions.preview.convert': '派生セッションを生成',
670689
'sessions.preview.converting': '変換中...',
671690
'sessions.preview.convert.loadedOnly': '読み込み済みのみ変換',
@@ -904,6 +923,11 @@ const ja = Object.freeze({
904923
'config.template.editFirst': '先にテンプレートを編集してから適用してください。',
905924
'config.template.bridgeCodexOnly': '{hint} テンプレートは Codex のみ編集可能です。',
906925
'config.localBridge.enabledCount': '{enabled}/{total} 件有効',
926+
'config.localBridge.poolSettings': 'プール設定',
927+
'config.localBridge.poolHint': '負荷分散に参加する候補プロバイダーを選択してください。',
928+
'config.localBridge.noProviders': '利用可能な候補プロバイダーがありません。先に直接プロバイダーを追加してください。',
929+
'config.transformProvider.title': '変換プロバイダーを使用',
930+
'config.providerBridgeHint': '{label} ブリッジモード',
907931
'config.template.openEditor': 'テンプレートエディタを開く',
908932
'modal.configTemplate.title': 'Config テンプレートエディタ(手動確認適用)',
909933
'modal.configTemplate.placeholder': 'ここに config.toml テンプレート内容を編集してください',
@@ -1374,6 +1398,12 @@ const ja = Object.freeze({
13741398
'settings.trash.retentionLabel': '保持日数',
13751399
'settings.trash.retentionUnit': '日',
13761400
'settings.trash.retentionHint': '範囲 1-365 日、デフォルト 30 日。ゴミ箱読み込み時に期限切れレコードを自動クリーンアップします。',
1401+
'settings.trash.count': '全 {count} 件',
1402+
'settings.trash.retentionShort': '保持 {days} 日',
1403+
'settings.trash.disabled': 'ゴミ箱は無効です',
1404+
'settings.trash.disabledHint': '削除済みセッションは直接完全削除され、ゴミ箱に移動しません。',
1405+
'settings.trash.clearShort': '空にする',
1406+
'settings.trash.loadMoreItems': 'さらに読み込む(残り {count} 件)',
13771407

13781408
'settings.webhook.title': 'Webhook',
13791409
'settings.webhook.meta': 'イベント通知を外部サービスへ送信',
@@ -1499,6 +1529,8 @@ const ja = Object.freeze({
14991529

15001530
// OpenClaw config panel
15011531
'openclaw.applyHint': '~/.openclaw/openclaw.json に書き込みます。JSON5 対応。',
1532+
'openclaw.workspace.title': 'OpenClaw ワークスペース',
1533+
'openclaw.configs.hint': 'よく使う設定を選択、またはエディタで完全な JSON5 を維持します。',
15021534
'openclaw.agents.hint': 'Workspace の AGENTS.md を読み書きします。デフォルトパス ~/.openclaw/workspace/AGENTS.md。',
15031535
'openclaw.agents.open': 'AGENTS.md を開く',
15041536
'openclaw.workspaceFile': 'ワークスペースファイル',

web-ui/modules/i18n/locales/vi.mjs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ const vi = Object.freeze({
5151
'common.refresh': 'Làm mới',
5252
'common.refreshing': 'Đang làm mới...',
5353
'common.loading': 'Đang tải...',
54+
'common.menu': 'Menu',
55+
'common.registry': 'Nguồn',
5456
'common.saving': 'Đang lưu...',
5557
'common.sending': 'Đang gửi...',
5658
'common.scanning': 'Đang quét...',
@@ -78,6 +80,7 @@ const vi = Object.freeze({
7880
'common.notConfigured': 'Chưa cấu hình',
7981
'common.enabled': 'Đã bật',
8082
'common.disabled': 'Đã tắt',
83+
'common.retry': 'Thử lại',
8184
'common.notSelected': 'Chưa chọn',
8285

8386
// Roles / labels
@@ -152,6 +155,11 @@ const vi = Object.freeze({
152155
'side.system.settings.meta': 'Dữ liệu / Sao lưu',
153156
'side.newTab': 'Tab mới',
154157
'config.localBridge.enabledCount': 'Đã bật {enabled}/{total}',
158+
'config.localBridge.poolSettings': 'Cài đặt pool',
159+
'config.localBridge.poolHint': 'Chọn nhà cung cấp ứng viên tham gia cân bằng tải.',
160+
'config.localBridge.noProviders': 'Không có nhà cung cấp ứng viên. Vui lòng thêm nhà cung cấp trực tiếp trước.',
161+
'config.transformProvider.title': 'Đang dùng nhà cung cấp biến đổi',
162+
'config.providerBridgeHint': 'Chế độ cầu nối {label}',
155163
'toolConfig.allow': 'Cho phép ghi',
156164
'toolConfig.viewOnly': 'Chỉ đọc',
157165
'toolConfig.enableWrite': 'Bật ghi',
@@ -434,6 +442,7 @@ const vi = Object.freeze({
434442
'toast.delete.ok': 'Đã xóa',
435443
'toast.delete.fail': 'Xóa thất bại',
436444
'toast.operation.success': 'Thao tác thành công',
445+
'toast.operation.fail': 'Thao tác thất bại',
437446
'toast.load.fail': 'Tải tệp thất bại',
438447
'toast.apply.success': 'Đã áp dụng cấu hình',
439448
'toast.apply.fail': 'Áp dụng cấu hình thất bại',
@@ -530,6 +539,7 @@ const vi = Object.freeze({
530539
'field.apiEndpoint': 'API endpoint',
531540
'field.apiKey': 'API key',
532541
'field.baseUrl': 'Base URL',
542+
'field.url': 'URL',
533543
'field.provider': 'Nhà cung cấp',
534544
'field.providerName': 'Tên nhà cung cấp',
535545
'field.modelName': 'Tên model',
@@ -1400,6 +1410,12 @@ const vi = Object.freeze({
14001410
'settings.trash.retentionMeta': 'Mục trong thùng rác cũ hơn số ngày lưu giữ sẽ tự động bị xóa',
14011411
'settings.trash.retentionLabel': 'Số ngày lưu giữ',
14021412
'settings.trash.retentionHint': 'Phạm vi 1-365 ngày, mặc định 30. Mục hết hạn bị xóa khi tải thùng rác.',
1413+
'settings.trash.count': 'Tổng {count} mục',
1414+
'settings.trash.retentionShort': 'Lưu {days} ngày',
1415+
'settings.trash.disabled': 'Thùng rác đã tắt',
1416+
'settings.trash.disabledHint': 'Phiên đã xóa sẽ bị xóa vĩnh viễn, không chuyển vào thùng rác nữa.',
1417+
'settings.trash.clearShort': 'Làm trống',
1418+
'settings.trash.loadMoreItems': 'Tải thêm (còn lại {count})',
14031419
'settings.templateConfirm.title': 'Xác nhận áp dụng template',
14041420
'settings.templateConfirm.meta': 'Giảm ghi nhầm',
14051421
'settings.templateConfirm.toggle': 'Xem trước diff trước khi áp dụng (Xác nhận → Áp dụng)',

0 commit comments

Comments
 (0)