dsh-config-manager 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/lib/adapters/self.d.ts +44 -4
- package/lib/adapters/self.js +63 -3
- package/lib/client.d.ts +11 -6
- package/lib/client.js +1263 -1192
- package/lib/core/analyzer.d.ts +3 -0
- package/lib/core/analyzer.js +32 -56
- package/lib/core/backup.d.ts +36 -0
- package/lib/core/backup.js +125 -3
- package/lib/core/import-defaults.d.ts +13 -0
- package/lib/core/import-defaults.js +14 -0
- package/lib/core/importer.d.ts +10 -1
- package/lib/core/importer.js +7 -1
- package/lib/core/messages.d.ts +4 -0
- package/lib/core/messages.js +8 -0
- package/lib/core/model-tools.js +19 -3
- package/lib/index.d.ts +1 -1
- package/lib/index.js +12 -22
- package/lib/routes/import.js +3 -1
- package/lib/routes/recovery.js +226 -2
- package/lib/routes/sync.js +26 -7
- package/lib/security/integrity.d.ts +24 -0
- package/lib/security/integrity.js +30 -0
- package/lib/security/unverified-file-blocks.d.ts +73 -0
- package/lib/security/unverified-file-blocks.js +151 -0
- package/lib/sync/autosync-config.d.ts +8 -4
- package/lib/sync/autosync-config.js +2 -2
- package/lib/sync/autosync-scheduler.d.ts +61 -3
- package/lib/sync/autosync-scheduler.js +87 -10
- package/lib/sync/config-issue-reporting.d.ts +32 -0
- package/lib/sync/config-issue-reporting.js +63 -0
- package/lib/sync/gc-skip.d.ts +42 -0
- package/lib/sync/gc-skip.js +57 -0
- package/lib/sync/git/git-transport.d.ts +10 -0
- package/lib/sync/git/git-transport.js +72 -14
- package/lib/sync/merge-preview-text.d.ts +11 -0
- package/lib/sync/merge-preview-text.js +38 -0
- package/lib/sync/s3/s3-transport.d.ts +8 -1
- package/lib/sync/s3/s3-transport.js +19 -5
- package/lib/sync/session-tombstones.d.ts +53 -0
- package/lib/sync/session-tombstones.js +52 -0
- package/lib/sync/sync-config.d.ts +55 -5
- package/lib/sync/sync-config.js +188 -15
- package/lib/sync/sync-engine.d.ts +21 -7
- package/lib/sync/sync-engine.js +63 -85
- package/lib/sync/webdav/webdav-transport.d.ts +11 -1
- package/lib/sync/webdav/webdav-transport.js +22 -6
- package/lib/ui/i18n.js +56 -36
- package/lib/ui/import-wizard.d.ts +1 -1
- package/lib/ui/import-wizard.js +2 -1
- package/lib/ui/overview-view.d.ts +6 -2
- package/lib/ui/overview-view.js +8 -1
- package/lib/ui/report.js +2 -1
- package/package.json +2 -2
- package/src/adapters/self.test.ts +126 -85
- package/src/adapters/self.ts +73 -6
- package/src/client/ConfigManagerSection.tsx +0 -3
- package/src/client/common/Icon.tsx +3 -0
- package/src/client/common/SectionComposition.tsx +6 -1
- package/src/client/common/info-hint-guard.test.ts +7 -5
- package/src/client/config-manager.module.css +254 -13
- package/src/client/environment/EnvironmentPanel.tsx +57 -47
- package/src/client/environment/maintenance/DiskUsageCard.tsx +3 -1
- package/src/client/export/ExportView.tsx +3 -1
- package/src/client/home/HomePanel.tsx +25 -5
- package/src/client/import/ImportWizardView.tsx +1 -3
- package/src/client/import/import-wizard-steps.tsx +1 -17
- package/src/client/library/ArtifactRow.tsx +13 -2
- package/src/client/library/LibraryPanel.tsx +12 -2
- package/src/client/locales.ts +53 -34
- package/src/client/lucide-icons.d.ts +2 -0
- package/src/client/market/MarketPanel.tsx +12 -11
- package/src/client/market/market-locales.ts +16 -16
- package/src/client/recovery/RollbackCard.tsx +4 -2
- package/src/client/recovery/recovery-locales.test.ts +4 -2
- package/src/client/recovery/recovery-locales.ts +23 -23
- package/src/client/sync/AutosyncCard.tsx +36 -10
- package/src/client/sync/SyncChannelEntryCard.tsx +18 -19
- package/src/client/sync/SyncPanel.tsx +32 -33
- package/src/client/sync/SyncProbeRow.tsx +23 -6
- package/src/client/sync/SyncSectionPickerDialog.tsx +2 -2
- package/src/client/sync/sync-locales.ts +24 -24
- package/src/core/analyzer.ts +37 -56
- package/src/core/backup.test.ts +139 -1
- package/src/core/backup.ts +145 -4
- package/src/core/import-defaults.ts +13 -0
- package/src/core/importer.ts +12 -2
- package/src/core/integrity-extra-entries.test.ts +205 -0
- package/src/core/messages.test.ts +3 -1
- package/src/core/messages.ts +8 -0
- package/src/core/model-tools.test.ts +33 -0
- package/src/core/model-tools.ts +20 -4
- package/src/index.ts +12 -21
- package/src/routes/import.ts +4 -1
- package/src/routes/recovery.test.ts +198 -1
- package/src/routes/recovery.ts +194 -1
- package/src/routes/sync.ts +27 -8
- package/src/security/integrity.ts +37 -0
- package/src/security/unverified-file-blocks.ts +192 -0
- package/src/sync/autosync-config.ts +9 -5
- package/src/sync/autosync-scheduler.test.ts +133 -6
- package/src/sync/autosync-scheduler.ts +118 -14
- package/src/sync/config-issue-reporting.test.ts +148 -0
- package/src/sync/config-issue-reporting.ts +75 -0
- package/src/sync/gc-skip.test.ts +66 -0
- package/src/sync/gc-skip.ts +64 -0
- package/src/sync/git/git-probe.test.ts +3 -2
- package/src/sync/git/git-transport.test.ts +168 -7
- package/src/sync/git/git-transport.ts +70 -14
- package/src/sync/merge-preview-text.ts +37 -0
- package/src/sync/s3/s3-delete-index-404.test.ts +26 -1
- package/src/sync/s3/s3-transport.ts +23 -4
- package/src/sync/session-tombstones.ts +90 -0
- package/src/sync/sync-config.test.ts +170 -1
- package/src/sync/sync-config.ts +184 -14
- package/src/sync/sync-engine.test.ts +238 -0
- package/src/sync/sync-engine.ts +79 -83
- package/src/sync/webdav/webdav-delete-index-404.test.ts +29 -1
- package/src/sync/webdav/webdav-transport.ts +26 -5
- package/src/ui/disk-usage-view.test.ts +2 -1
- package/src/ui/i18n.test.ts +47 -0
- package/src/ui/i18n.ts +58 -38
- package/src/ui/import-wizard.test.ts +18 -1
- package/src/ui/import-wizard.ts +3 -2
- package/src/ui/overview-view.test.ts +9 -0
- package/src/ui/overview-view.ts +9 -2
- package/src/ui/report.test.ts +4 -3
- package/src/ui/report.ts +2 -1
|
@@ -97,16 +97,8 @@ export function SelectStep(props: {
|
|
|
97
97
|
onReset: () => void
|
|
98
98
|
apiT: UiT
|
|
99
99
|
t: TranslateNS<'config-manager'>
|
|
100
|
-
/**
|
|
101
|
-
* 「从其它 agent 导入」入口(t17,**可选**):壳层注入 uiT 与导航回调时才渲染。
|
|
102
|
-
*
|
|
103
|
-
* 为什么放在本步而不是加一级页签:导入是 Task 面板里的多阶段流程,外部来源只是
|
|
104
|
-
* 「包从哪来」的另一种选择 —— 与「选择 ZIP 文件」并列,用户心智上是同一件事的两个入口。
|
|
105
|
-
* 老调用方不传该 prop → 本区块不渲染,既有行为逐字不变。
|
|
106
|
-
*/
|
|
107
|
-
foreignEntry?: { uiT: UiT; onOpen: () => void }
|
|
108
100
|
}) {
|
|
109
|
-
const { fileInput, selectModel, uploading, error, onCancel, onPickFile, onReset, apiT, t
|
|
101
|
+
const { fileInput, selectModel, uploading, error, onCancel, onPickFile, onReset, apiT, t } = props
|
|
110
102
|
return (
|
|
111
103
|
<div className={`${css.viewBody} ${css.sparseFill}`}>
|
|
112
104
|
<SectionTitle title={t('import.select.title')} info={<InfoHint text={t('import.select.hint')} label={t('common.infoHint')} />} />
|
|
@@ -143,14 +135,6 @@ export function SelectStep(props: {
|
|
|
143
135
|
</Button>
|
|
144
136
|
</div>
|
|
145
137
|
{error !== null && <ErrorBanner error={error} onRetry={onReset} t={apiT} />}
|
|
146
|
-
{foreignEntry !== undefined && (
|
|
147
|
-
<>
|
|
148
|
-
<div className={css.actionRow} style={{ marginBottom: 0 }}>
|
|
149
|
-
<Button onClick={foreignEntry.onOpen}>{t('foreign.source.title')}</Button>
|
|
150
|
-
<InfoHint text={t('foreign.source.hint')} label={t('common.infoHint')} />
|
|
151
|
-
</div>
|
|
152
|
-
</>
|
|
153
|
-
)}
|
|
154
138
|
</div>
|
|
155
139
|
)
|
|
156
140
|
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from '../../ui/artifact-view.ts'
|
|
19
19
|
import type { UiT } from '../../ui/i18n.ts'
|
|
20
20
|
import type { TranslateNS } from '../client-types.ts'
|
|
21
|
+
import { midEllipsis } from '../../ui/mid-ellipsis.ts'
|
|
21
22
|
import { Badge, Button, Spinner } from '../common/ui.tsx'
|
|
22
23
|
import { ExpandChevron } from '../common/Icon.tsx'
|
|
23
24
|
import { MoreMenu } from '../common/MoreMenu.tsx'
|
|
@@ -104,7 +105,14 @@ export function ArtifactRowView({
|
|
|
104
105
|
<div className={css.artifactTitle}>
|
|
105
106
|
{/* 来源标签上标题行(定案 B):来源已由此表达,再放一个 kind 图标属重复表达 */}
|
|
106
107
|
<span className={css.kindTag}>{uiT(ARTIFACT_KIND_LABEL_KEY[row.kind])}</span>
|
|
107
|
-
|
|
108
|
+
{/* 文件名走等宽栈 + 中段省略(DESIGN.md:93 / §9-5):尾部才是区分信息
|
|
109
|
+
(同一分钟导出的两份备份只差尾部时间戳),尾部截断会让两行看起来一样。 */}
|
|
110
|
+
<span
|
|
111
|
+
className={row.title.kind === 'text' ? `${css.artifactName} ${css.mono}` : css.artifactName}
|
|
112
|
+
title={name}
|
|
113
|
+
>
|
|
114
|
+
{row.title.kind === 'text' ? midEllipsis(name) : name}
|
|
115
|
+
</span>
|
|
108
116
|
</div>
|
|
109
117
|
{/* 元数据是**自由序列**而不是表格列:某一段没有(如快照没有体积)就整段不出现,
|
|
110
118
|
绝不补一个「—」占位 —— 这里没有对齐问题,占位只会变成噪音(§3 第三条) */}
|
|
@@ -127,7 +135,10 @@ export function ArtifactRowView({
|
|
|
127
135
|
{primary !== undefined && (
|
|
128
136
|
<Button
|
|
129
137
|
size="sm"
|
|
130
|
-
|
|
138
|
+
// 常态 ghost:八行八颗实心按钮会把 accent 变成壁纸,整屏最亮的是 chrome。
|
|
139
|
+
// 悬停/展开时由 CSS(.rowPrimary)升到 primary —— 那时它才真是「这一行要做的事」。
|
|
140
|
+
variant={primary === 'delete' ? 'danger' : 'ghost'}
|
|
141
|
+
className={primary === 'delete' ? undefined : css.rowPrimary}
|
|
131
142
|
// 进行中图标由 Button 原语按 loading 自动渲染(DESIGN §14.11:不要手写 Spinner)
|
|
132
143
|
loading={pendingPrimary}
|
|
133
144
|
disabled={pending !== null && !pendingPrimary}
|
|
@@ -283,7 +283,14 @@ export function LibraryPanel({ api, syncApi, marketApi, t, uiT, onAction, onOpen
|
|
|
283
283
|
|
|
284
284
|
{/* 计数条:**不显示合计体积** —— 四源里只有备份文件有体积,求和会把「未知」混进「合计」(§6.8 ①) */}
|
|
285
285
|
<div className={css.libraryFooter}>
|
|
286
|
-
|
|
286
|
+
{/* 计数只在筛选/搜索生效时出现(2026-11-01):无筛选时它与上方「全部 16」是同一个数
|
|
287
|
+
出现在同一屏的两处 —— app-screens §5「一个数字只出现一处」。筛掉多少要看得出
|
|
288
|
+
「眼前几条 / 一共几条」,所以这时写成分数而不是重复总数。 */}
|
|
289
|
+
{filtered.length !== rows.length && (
|
|
290
|
+
<span className={css.libraryCount}>
|
|
291
|
+
{t('library.summary.filtered', { shown: filtered.length, total: rows.length })}
|
|
292
|
+
</span>
|
|
293
|
+
)}
|
|
287
294
|
{/* 加密计数只统计**当前筛选后**的行:此前用 rows(全部来源)⇒ 切到「本机快照」等
|
|
288
295
|
没有加密项的类别时底部仍显示「1 个加密」,与眼前看到的列表不符(真机反馈)。 */}
|
|
289
296
|
{encryptedCount > 0 && (
|
|
@@ -291,7 +298,10 @@ export function LibraryPanel({ api, syncApi, marketApi, t, uiT, onAction, onOpen
|
|
|
291
298
|
)}
|
|
292
299
|
<InfoHint text={t('backupFiles.hint')} label={t('common.infoHint')} />
|
|
293
300
|
<span className={css.statusSpacer} />
|
|
294
|
-
|
|
301
|
+
{/* 底栏四个入口,只有「导出」是这一页的产品级动作(把本机配置打成可分享的包);
|
|
302
|
+
其余三个是「拿别人的东西进来 / 去别处找」。主按钮只留一个,否则整屏没有焦点
|
|
303
|
+
(app-screens §13:强调色应用在数据与至多一个控件上)。 */}
|
|
304
|
+
<Button size="sm" variant="primary" onClick={onOpenExport}>{t('library.new.export')}</Button>
|
|
295
305
|
<Button size="sm" onClick={() => { onOpenTask('import') }}>{t('library.new.import')}</Button>
|
|
296
306
|
<Button size="sm" onClick={() => { onOpenTask('market') }}>{t('library.new.market')}</Button>
|
|
297
307
|
<Button size="sm" onClick={onOpenPublish}>{t('task.title.publish')}</Button>
|
package/src/client/locales.ts
CHANGED
|
@@ -65,6 +65,7 @@ export const zh = {
|
|
|
65
65
|
'overview.health.noBackup': '尚无任何备份',
|
|
66
66
|
'overview.health.scheduleFailed': '上次定时备份失败,建议检查备份设置',
|
|
67
67
|
'overview.health.recovery': '存在未解决的恢复事项,需要处理',
|
|
68
|
+
'overview.health.autoBackupOff': '自动备份已关闭,目前只有手动备份',
|
|
68
69
|
'overview.health.recoveryAction': '前往「事故恢复」处理',
|
|
69
70
|
'overview.metric.backups': '备份文件',
|
|
70
71
|
'overview.metric.snapshots': '安全快照',
|
|
@@ -95,6 +96,10 @@ export const zh = {
|
|
|
95
96
|
'overview.quick.backupSkippedOther': '已跳过:备份服务当前无法执行(详情见宿主日志)',
|
|
96
97
|
'overview.quick.backupFailed': '备份失败:{message}',
|
|
97
98
|
'overview.activity.title': '最近活动',
|
|
99
|
+
'overview.activity.colTime': '时间',
|
|
100
|
+
'overview.activity.colEvent': '动作',
|
|
101
|
+
'overview.activity.colDetail': '详情',
|
|
102
|
+
'overview.activity.colResult': '结果',
|
|
98
103
|
'overview.activity.empty': '暂无操作记录',
|
|
99
104
|
'overview.activity.unavailable': '活动记录读取失败(这不是「没有记录」)',
|
|
100
105
|
// 首屏 5 个只读请求有失败时如实说清(此前失败被吞成 null,页面仍显示绿色健康 + 空活动)
|
|
@@ -492,6 +497,9 @@ export const zh = {
|
|
|
492
497
|
'profiles.running.port': '端口 {port}',
|
|
493
498
|
'profiles.running.open': '打开',
|
|
494
499
|
'profiles.running.badge': ':{port} 运行中',
|
|
500
|
+
// 行徽章专用:状态由相邻的「运行中」ok 徽章表达,这里只回答「在哪个端口」
|
|
501
|
+
// (2026-11-01:原先把 ':{port} 运行中' 与它并排,同一个词三格内出现两次)。
|
|
502
|
+
'profiles.running.portBadge': ':{port}',
|
|
495
503
|
'profiles.running.originCurrent': '当前实例',
|
|
496
504
|
'profiles.running.originOwned': '本插件启动',
|
|
497
505
|
'profiles.running.originExternal': '其他实例',
|
|
@@ -800,6 +808,7 @@ export const zh = {
|
|
|
800
808
|
'library.remoteDeleteBaseline': '注意:这是当前的同步基线(本机上次同步到的那份)。删除不会改动本机配置,但下次同步会重新比对远端最新快照。',
|
|
801
809
|
'library.remoteDeleted': '已删除远端快照',
|
|
802
810
|
'library.summary.count': '共 {count} 个',
|
|
811
|
+
'library.summary.filtered': '{shown} / {total} 个',
|
|
803
812
|
'library.new.export': '手动导出',
|
|
804
813
|
'library.new.import': '从文件导入',
|
|
805
814
|
'library.new.market': '逛市场',
|
|
@@ -957,6 +966,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
957
966
|
'overview.health.noBackup': 'No backups yet',
|
|
958
967
|
'overview.health.scheduleFailed': 'The last scheduled backup failed — check backup settings',
|
|
959
968
|
'overview.health.recovery': 'Unresolved recovery items need attention',
|
|
969
|
+
'overview.health.autoBackupOff': 'Auto backup is off — this machine only has manual backups',
|
|
960
970
|
'overview.health.recoveryAction': 'Go to Incident recovery',
|
|
961
971
|
'overview.metric.backups': 'Backups',
|
|
962
972
|
'overview.metric.snapshots': 'Snapshots',
|
|
@@ -987,10 +997,14 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
987
997
|
'overview.quick.backupSkippedOther': 'Skipped: the backup service cannot run right now (see host logs for details)',
|
|
988
998
|
'overview.quick.backupFailed': 'Backup failed: {message}',
|
|
989
999
|
'overview.activity.title': 'Recent activity',
|
|
1000
|
+
'overview.activity.colTime': 'Time',
|
|
1001
|
+
'overview.activity.colEvent': 'Action',
|
|
1002
|
+
'overview.activity.colDetail': 'Detail',
|
|
1003
|
+
'overview.activity.colResult': 'Result',
|
|
990
1004
|
'overview.activity.empty': 'No activity yet',
|
|
991
1005
|
'overview.activity.unavailable': 'The activity log could not be read (this is not \"no activity\")',
|
|
992
1006
|
// Say it plainly when some of the five read-only requests failed (failures used to be swallowed into null)
|
|
993
|
-
'overview.loadFailed': '{count} data request
|
|
1007
|
+
'overview.loadFailed': '{count} data {count, plural, request|requests} failed: {message} — the metrics below may be incomplete',
|
|
994
1008
|
'overview.empty.title': 'Start protecting your configuration',
|
|
995
1009
|
'overview.empty.step1': 'Create a backup',
|
|
996
1010
|
'overview.empty.step2': 'Export the ZIP',
|
|
@@ -1034,7 +1048,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
1034
1048
|
'export.noteHint': 'The note is shown in the local backup list (and travels with the config backup) so you can tell exports apart.',
|
|
1035
1049
|
'export.compositionTitle': 'This export',
|
|
1036
1050
|
'export.compositionHint': 'Estimated from the current selection (read-only, zero writes)',
|
|
1037
|
-
'export.compositionPartial': '{sections} section
|
|
1051
|
+
'export.compositionPartial': '{sections} {sections, plural, section|sections} · {units} {units, plural, item|items} read · ~{size} (includes sections not read yet; the real total is no less)',
|
|
1038
1052
|
'export.compositionEmpty': 'Nothing selected to export',
|
|
1039
1053
|
// —— Content picker (Phase 1: per-item export selection) ——
|
|
1040
1054
|
'picker.title': 'Choose what to export',
|
|
@@ -1050,8 +1064,8 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
1050
1064
|
'picker.locked': 'Bundle',
|
|
1051
1065
|
'picker.lockedHint': 'Must be exported together with its group (e.g. a pnpm patch declaration and its patch file); clearing either clears the whole group.',
|
|
1052
1066
|
'picker.selectedOf': '{selected}/{total} selected',
|
|
1053
|
-
'picker.fileCount': '{count} file
|
|
1054
|
-
'picker.summary': 'Exporting {sections} section
|
|
1067
|
+
'picker.fileCount': '{count} {count, plural, file|files}',
|
|
1068
|
+
'picker.summary': 'Exporting {sections} {sections, plural, section|sections} · {units} {units, plural, item|items} · ~{size}',
|
|
1055
1069
|
'picker.loading': 'Reading exportable items…',
|
|
1056
1070
|
'picker.loadingItems': 'Reading items…',
|
|
1057
1071
|
'picker.loadingSection': 'Loading…',
|
|
@@ -1062,7 +1076,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
1062
1076
|
'picker.deviceSpecific': 'Device-specific',
|
|
1063
1077
|
'picker.sensitive': 'Sensitive',
|
|
1064
1078
|
'picker.showAll': 'Show all ({count})',
|
|
1065
|
-
'picker.summaryImport': '{sections} section
|
|
1079
|
+
'picker.summaryImport': '{sections} {sections, plural, section|sections} · {units} {units, plural, item|items}',
|
|
1066
1080
|
// —— Plugin tree embedded in the Extensions group (plugin → list entry / data dirs / large files) ——
|
|
1067
1081
|
'picker.pluginRoot': 'Plugins',
|
|
1068
1082
|
'picker.pluginLeafPlugin': 'Plugin list entry',
|
|
@@ -1114,21 +1128,21 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
1114
1128
|
'import.compatibility.title': 'Compatibility',
|
|
1115
1129
|
'import.compatibility.score': 'Compatibility score: {score}',
|
|
1116
1130
|
'import.preview.title': 'Import Preview',
|
|
1117
|
-
'import.preview.willChange': '{count} item
|
|
1118
|
-
'import.preview.unchanged': '{count} item
|
|
1119
|
-
'import.preview.settings': '{count} settings update
|
|
1120
|
-
'import.preview.plugins': '{count} plugin
|
|
1121
|
-
'import.preview.mcp': '{count} MCP server
|
|
1122
|
-
'import.preview.prompts': '{count} prompt
|
|
1123
|
-
'import.preview.paths': '{count} path mapping
|
|
1131
|
+
'import.preview.willChange': '{count} {count, plural, item|items} will change',
|
|
1132
|
+
'import.preview.unchanged': '{count} {count, plural, item|items} already identical (skipped)',
|
|
1133
|
+
'import.preview.settings': '{count} settings {count, plural, update|updates}',
|
|
1134
|
+
'import.preview.plugins': '{count} {count, plural, plugin|plugins} to install',
|
|
1135
|
+
'import.preview.mcp': '{count} MCP {count, plural, server|servers} added',
|
|
1136
|
+
'import.preview.prompts': '{count} {count, plural, prompt|prompts}',
|
|
1137
|
+
'import.preview.paths': '{count} path {count, plural, mapping|mappings} needed',
|
|
1124
1138
|
'import.preview.rebase': 'Base path rebased automatically (the backup came from a machine with a different DSH base path): {from} -> {to}; no manual mapping needed',
|
|
1125
|
-
'import.preview.secrets': '{count} secret
|
|
1126
|
-
'import.preview.conflicts': '{count} conflict
|
|
1139
|
+
'import.preview.secrets': '{count} {count, plural, secret|secrets} to re-enter',
|
|
1140
|
+
'import.preview.conflicts': '{count} {count, plural, conflict|conflicts}',
|
|
1127
1141
|
'import.preview.restart': 'A DSH restart is required after import',
|
|
1128
1142
|
'import.confirm.execute': 'Confirm Import',
|
|
1129
1143
|
'import.confirm.warning': 'Import modifies the current DSH configuration; a snapshot is taken first.',
|
|
1130
1144
|
'import.confirm.warningNoRollback': 'Import modifies the current DSH configuration; a snapshot is taken first. Bulk rollback is off — a failure does not stop the rest, and writes are not reverted.',
|
|
1131
|
-
'import.confirm.encrypted': 'The backup password will decrypt and restore {count} credential
|
|
1145
|
+
'import.confirm.encrypted': 'The backup password will decrypt and restore {count} {count, plural, credential|credentials}.',
|
|
1132
1146
|
'import.rollbackOnError': 'Roll back everything on failure (recommended)',
|
|
1133
1147
|
'import.log.title': 'Import Log',
|
|
1134
1148
|
'import.log.empty': 'Waiting for command output…',
|
|
@@ -1161,7 +1175,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
1161
1175
|
'import.conflicts.detailCurrent': 'Current',
|
|
1162
1176
|
'import.conflicts.detailImported': 'Backup',
|
|
1163
1177
|
'import.conflicts.empty': 'No conflicts',
|
|
1164
|
-
'import.conflicts.unresolved': '{count} item
|
|
1178
|
+
'import.conflicts.unresolved': '{count} {count, plural, item|items} unresolved',
|
|
1165
1179
|
'import.conflicts.keepCurrentAll': 'Keep all current',
|
|
1166
1180
|
'import.conflicts.useImportedAll': 'Use all backup',
|
|
1167
1181
|
'import.paths.title': 'Path Mapping',
|
|
@@ -1171,7 +1185,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
1171
1185
|
'import.paths.kind.missing': 'Set a new location',
|
|
1172
1186
|
'import.paths.kind.platformMismatch': 'Cross-platform path',
|
|
1173
1187
|
'import.paths.kind.homeMismatch': 'Different base path',
|
|
1174
|
-
'import.paths.unresolved': '{count} path
|
|
1188
|
+
'import.paths.unresolved': '{count} {count, plural, path|paths} unmapped (blank = skip that path, you can continue)',
|
|
1175
1189
|
'import.paths.empty': 'No paths to map',
|
|
1176
1190
|
'import.paths.browse': 'Choose a local path',
|
|
1177
1191
|
'import.paths.picker.title': 'Choose a local path',
|
|
@@ -1210,7 +1224,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
1210
1224
|
'snapshots.failed': 'Failed',
|
|
1211
1225
|
'snapshots.skipped': 'Skipped',
|
|
1212
1226
|
// —— git-style restore preview (line-by-line) ——
|
|
1213
|
-
'snapshots.plan.changed': '{count} file
|
|
1227
|
+
'snapshots.plan.changed': '{count} {count, plural, file|files} to restore',
|
|
1214
1228
|
'snapshots.plan.added': '{count} added',
|
|
1215
1229
|
'snapshots.plan.deleted': '{count} to delete',
|
|
1216
1230
|
'snapshots.plan.plugins': '{count} plugin uninstall',
|
|
@@ -1274,7 +1288,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
1274
1288
|
'backupSchedule.status.success': 'Success',
|
|
1275
1289
|
'backupSchedule.status.skipped': 'Skipped',
|
|
1276
1290
|
'backupSchedule.status.failed': 'Failed',
|
|
1277
|
-
'backupSchedule.consecutiveFailures': 'Scheduled backups failed {count} time
|
|
1291
|
+
'backupSchedule.consecutiveFailures': 'Scheduled backups failed {count} {count, plural, time|times} in a row — check the configuration (disk space / permissions / section export errors)',
|
|
1278
1292
|
'backupSchedule.loading': 'Loading scheduled backup settings…',
|
|
1279
1293
|
'backupSchedule.error': 'Failed to load scheduled backup settings',
|
|
1280
1294
|
// —— Retention policy (m-retention: GFS tiers) ——
|
|
@@ -1364,6 +1378,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
1364
1378
|
'profiles.running.port': 'port {port}',
|
|
1365
1379
|
'profiles.running.open': 'Open',
|
|
1366
1380
|
'profiles.running.badge': ':{port} running',
|
|
1381
|
+
'profiles.running.portBadge': ':{port}',
|
|
1367
1382
|
'profiles.running.originCurrent': 'current instance',
|
|
1368
1383
|
'profiles.running.originOwned': 'launched here',
|
|
1369
1384
|
'profiles.running.originExternal': 'other instance',
|
|
@@ -1406,9 +1421,12 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
1406
1421
|
'profiles.list.title': 'All profiles',
|
|
1407
1422
|
'profiles.list.count': '{count} total',
|
|
1408
1423
|
'profiles.list.hint': 'Click a profile row for full details (bundle layers / dependencies / raw patch)',
|
|
1409
|
-
|
|
1424
|
+
// 英文侧不再用 (s)/(ies) 括号式复数(2026-11-01):档案只有 1 个时它渲染成
|
|
1425
|
+
// "1 layer(s) · 5 patch entry(ies)",在一行被截断的元信息里既占宽又不专业。
|
|
1426
|
+
// zh 侧本来就是无复数形态,改成同样的「数字 + 名词」短形态后两侧结构一致。
|
|
1427
|
+
'profiles.row.summary': '{bundles} layers · {patch} patches · {deps} deps',
|
|
1410
1428
|
'profiles.detail.info': 'Profile info',
|
|
1411
|
-
'profiles.bundles.count': '{count} bundle layer
|
|
1429
|
+
'profiles.bundles.count': '{count} bundle {count, plural, layer|layers}',
|
|
1412
1430
|
'profiles.shape.web': 'Web',
|
|
1413
1431
|
'profiles.shape.headless': 'Headless',
|
|
1414
1432
|
'profiles.shape.generic': 'Custom',
|
|
@@ -1418,7 +1436,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
1418
1436
|
'profiles.incomplete.startedAt': 'started at {time}',
|
|
1419
1437
|
'profiles.incomplete.dir': 'Directory {path}',
|
|
1420
1438
|
'profiles.incomplete.hint': 'This is an interrupted profile copy: it has no package.json and cannot be launched. Delete it if you no longer need it (only this directory is removed).',
|
|
1421
|
-
'profiles.list.incompleteCount': '{count} incomplete
|
|
1439
|
+
'profiles.list.incompleteCount': '{count} incomplete {count, plural, copyy|copyies} (not launchable)',
|
|
1422
1440
|
'profiles.delete.incompleteMessage': 'Delete the incomplete copy “{name}”? The whole directory (including partially copied files) will be physically removed and cannot be recovered.',
|
|
1423
1441
|
'profiles.issue.manifestInvalid': 'package.json is corrupted',
|
|
1424
1442
|
'profiles.issue.patchTooLarge': 'patch file too large (content not shown)',
|
|
@@ -1426,7 +1444,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
1426
1444
|
'profiles.deps': 'Dependencies',
|
|
1427
1445
|
'profiles.patchReload.live': 'patch hot-reloads',
|
|
1428
1446
|
'profiles.patchReload.startup': 'patch applies at startup only',
|
|
1429
|
-
'profiles.patchEntries': '{count} patch
|
|
1447
|
+
'profiles.patchEntries': '{count} patch {count, plural, entryy|entryies}',
|
|
1430
1448
|
'profiles.bundles.none': '(none)',
|
|
1431
1449
|
'profiles.nodeModules.yes': 'node_modules present',
|
|
1432
1450
|
'profiles.nodeModules.no': 'no node_modules',
|
|
@@ -1475,10 +1493,10 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
1475
1493
|
'profiles.error.instanceRunning': 'An instance of this profile is running — stop it before deleting',
|
|
1476
1494
|
'profiles.error.generic': 'Operation failed: {message}',
|
|
1477
1495
|
// —— Import compatibility (Workbench Rebuild) ——
|
|
1478
|
-
'import.compatibility.sections': '{count} section
|
|
1496
|
+
'import.compatibility.sections': '{count} {count, plural, section|sections}',
|
|
1479
1497
|
'import.compatibility.plugins': 'installed {installed} · to install {toInstall}',
|
|
1480
|
-
'import.compatibility.paths': '{count} path issue
|
|
1481
|
-
'import.compatibility.secrets': '{count} secret
|
|
1498
|
+
'import.compatibility.paths': '{count} path {count, plural, issue|issues}',
|
|
1499
|
+
'import.compatibility.secrets': '{count} {count, plural, secret|secrets} to re-enter',
|
|
1482
1500
|
'import.compatibility.score.excellent': 'Excellent',
|
|
1483
1501
|
'import.compatibility.score.good': 'Good',
|
|
1484
1502
|
'import.compatibility.score.partial': 'Partial',
|
|
@@ -1501,7 +1519,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
1501
1519
|
// After the restart the banner must stop asking for one (real-machine feedback)
|
|
1502
1520
|
'shell.running.restore': 'Snapshot restore',
|
|
1503
1521
|
'shell.status.idle': 'Idle',
|
|
1504
|
-
'shell.status.running': '{count} task
|
|
1522
|
+
'shell.status.running': '{count} {count, plural, task|tasks} running',
|
|
1505
1523
|
'shell.status.recovery': 'Recovery pending',
|
|
1506
1524
|
// —— Run center (the drawer's "In progress" segment; terminate = cooperative cancel) ——
|
|
1507
1525
|
'shell.status.runningAction': 'View running tasks',
|
|
@@ -1565,7 +1583,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
1565
1583
|
'overview.result.skipped': 'Skipped',
|
|
1566
1584
|
'overview.activity.copy': 'Copy',
|
|
1567
1585
|
'overview.activity.redacted': '(filename redacted)',
|
|
1568
|
-
'overview.sections.entries': '{count} item
|
|
1586
|
+
'overview.sections.entries': '{count} {count, plural, item|items}',
|
|
1569
1587
|
'overview.nav.activity': 'Activity',
|
|
1570
1588
|
'overview.nav.about': 'About',
|
|
1571
1589
|
// ---------- session grouping (issue #45) ----------
|
|
@@ -1628,13 +1646,13 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
1628
1646
|
'profiles.version.format': 'session format v{version}',
|
|
1629
1647
|
'profiles.format.newer': 'its conversations are hidden here',
|
|
1630
1648
|
'profiles.format.older': 'conversations here are hidden there',
|
|
1631
|
-
'nextSteps.sessionsRestart.hint': '{count} session file
|
|
1649
|
+
'nextSteps.sessionsRestart.hint': '{count} session {count, plural, file|files} written. DSH builds the session list and workspace projection only at startup — on Desktop quit and reopen the app, for CLI profiles restart dsh.',
|
|
1632
1650
|
'import.source.title': 'Source & compatibility',
|
|
1633
1651
|
'import.source.dsh': 'DSH version: {source} → local {target}',
|
|
1634
1652
|
'import.source.platform': 'Platform: {source} → local {target}',
|
|
1635
1653
|
'import.source.schema': 'Backup format: v{version}',
|
|
1636
1654
|
'import.source.reasons': 'Why this score',
|
|
1637
|
-
'import.sessionFormat.skipped': '{count} session
|
|
1655
|
+
'import.sessionFormat.skipped': '{count} {count, plural, session was|sessions were} not imported because of the \"skip\" disposition (their format is newer than this DSH build — they would not be visible anyway)',
|
|
1638
1656
|
'import.sessionFormat.retryPlan': 'Regenerate plan',
|
|
1639
1657
|
'import.sessionFormat.option.abort': 'Abort the import (recommended)',
|
|
1640
1658
|
'import.sessionFormat.hint.abort': 'Stops at the planning stage and writes nothing.',
|
|
@@ -1644,7 +1662,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
1644
1662
|
'import.sessionFormat.hint.guide': 'Session files are written as usual, but this DSH build will not show those conversations.',
|
|
1645
1663
|
'import.sessionFormat.title': 'Sessions this DSH build cannot read',
|
|
1646
1664
|
'import.sessionFormat.blocked': 'Stopped by \"abort\" — nothing was written. Pick \"skip\" or \"warn only\" and regenerate the plan.',
|
|
1647
|
-
'import.sessionFormat.summary': '{count} session log
|
|
1665
|
+
'import.sessionFormat.summary': '{count} {count, plural, session log is|session logs are} v{newer} while this build supports v{target}: DSH skips them silently — no error, and they never appear in the workspace',
|
|
1648
1666
|
'import.sessionFormat.sampled': 'The format check was sampled: {checked} checked, {skipped} not checked (unchecked ones are treated as readable and are not affected by this disposition)',
|
|
1649
1667
|
'import.sessionFormat.guide.upgrade': 'Upgrade the DSH build of this profile to one that supports v{newer} (this build supports v{target}) and import again; or re-export on the source machine with a DSH build no newer than v{target}.',
|
|
1650
1668
|
'import.sessionFormat.guide.doctor': 'Inspect the sessions already on this machine (read-only):',
|
|
@@ -1677,6 +1695,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
1677
1695
|
'library.remoteDeleteBaseline': 'Note: this is the current sync baseline (the snapshot this machine last synced to). Deleting it does not change this machine, but the next sync will re-compare against the latest remote snapshot.',
|
|
1678
1696
|
'library.remoteDeleted': 'Remote snapshot deleted',
|
|
1679
1697
|
'library.summary.count': '{count} total',
|
|
1698
|
+
'library.summary.filtered': '{shown} of {total}',
|
|
1680
1699
|
'library.new.export': 'Export',
|
|
1681
1700
|
'library.new.import': 'Import from file',
|
|
1682
1701
|
'library.new.market': 'Browse market',
|
|
@@ -1686,7 +1705,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
1686
1705
|
// S7: rollback points (workspace + session rewind) command title
|
|
1687
1706
|
'palette.rescue.rollback': 'Rollback points (workspace + session rewind)',
|
|
1688
1707
|
'import.reason.schemaUnsupported': 'The bundle uses schema v{version}, beyond what this plugin supports',
|
|
1689
|
-
'import.reason.missingSections': 'Declared by the backup but missing from the archive: {count} section
|
|
1708
|
+
'import.reason.missingSections': 'Declared by the backup but missing from the archive: {count} {count, plural, section|sections} — {sections}',
|
|
1690
1709
|
'import.reason.sourceNewer': 'Backup comes from a newer DSH ({source} → local {target}): it may contain settings this build does not know',
|
|
1691
1710
|
'import.reason.sourceOlder': 'Backup comes from an older DSH ({source} → local {target}): backward compatible',
|
|
1692
1711
|
// ---------- Importing from other agents (2026-10-04) ----------
|
|
@@ -1737,9 +1756,9 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
1737
1756
|
'foreign.lossy.hint': 'These three behaviours are built into the "external config → bundle → import" pipeline: none of them blocks the import, and the compatibility score never explains them.',
|
|
1738
1757
|
'foreign.lossy.applies': 'Applies here',
|
|
1739
1758
|
'foreign.lossy.notApplies': 'Not applicable here',
|
|
1740
|
-
'foreign.credentialRefs': '{count} credential
|
|
1759
|
+
'foreign.credentialRefs': '{count} {count, plural, credential|credentials} to re-enter (values never travel)',
|
|
1741
1760
|
'foreign.credentialRefsNone': 'No credentials to enter',
|
|
1742
|
-
'foreign.skipped': '{count} item
|
|
1761
|
+
'foreign.skipped': '{count} {count, plural, item|items} not migrated',
|
|
1743
1762
|
'foreign.skip.unsupported-hooks': 'settings hooks: DSH has no equivalent, not migrated',
|
|
1744
1763
|
'foreign.skip.unsupported-commands': 'Slash commands: not the same thing as DSH prompts, not migrated this round',
|
|
1745
1764
|
'foreign.skip.credentials-not-migrated': 'Credential fields: only the names are recorded; the values are never read or migrated',
|
|
@@ -38,6 +38,8 @@ declare module 'lucide-react/dist/esm/icons/lock.mjs' { const i: LucideIconCompo
|
|
|
38
38
|
declare module 'lucide-react/dist/esm/icons/message-square.mjs' { const i: LucideIconComponent; export default i }
|
|
39
39
|
declare module 'lucide-react/dist/esm/icons/package-check.mjs' { const i: LucideIconComponent; export default i }
|
|
40
40
|
declare module 'lucide-react/dist/esm/icons/pencil.mjs' { const i: LucideIconComponent; export default i }
|
|
41
|
+
// 测试连接(同步页动作行的 28px 图标按钮)
|
|
42
|
+
declare module 'lucide-react/dist/esm/icons/plug.mjs' { const i: LucideIconComponent; export default i }
|
|
41
43
|
declare module 'lucide-react/dist/esm/icons/refresh-cw.mjs' { const i: LucideIconComponent; export default i }
|
|
42
44
|
declare module 'lucide-react/dist/esm/icons/rotate-cw.mjs' { const i: LucideIconComponent; export default i }
|
|
43
45
|
declare module 'lucide-react/dist/esm/icons/search.mjs' { const i: LucideIconComponent; export default i }
|
|
@@ -473,18 +473,19 @@ export function MarketPanel({ api, myConfigsApi, importApi, syncApi, t, cmT, ope
|
|
|
473
473
|
{!downloadOpen && (<>
|
|
474
474
|
<SectionTitle title={t('section.label')} info={<InfoHint text={t('section.description')} label={t('common.infoHint')} />} />
|
|
475
475
|
|
|
476
|
-
{/*
|
|
477
|
-
|
|
476
|
+
{/* 市场操作行(2026-11-01):原为一张卡,卡里只有标题 + 两个按钮,右侧四百多像素永远空着
|
|
477
|
+
(app-screens §4.2 卡内死空间,白吃约 50px)。改成标题与动作同一行 —— 它本来就是
|
|
478
|
+
「对仓库做两件事」,不是一块独立内容区。 */}
|
|
479
|
+
<div className={css.marketRepoRow}>
|
|
478
480
|
<span className={css.groupLabel}>{t('config.title')}</span>
|
|
479
|
-
<
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
</Card>
|
|
481
|
+
<span className={css.statusSpacer} />
|
|
482
|
+
<Button variant="primary" loading={state.refreshing} disabled={state.importing || state.browsing} onClick={() => { void runRefresh() }}>
|
|
483
|
+
{t('config.refresh')}
|
|
484
|
+
</Button>
|
|
485
|
+
<Button loading={state.browsing} disabled={state.refreshing || state.importing} onClick={() => { void runBrowse() }}>
|
|
486
|
+
{t('list.browse')}
|
|
487
|
+
</Button>
|
|
488
|
+
</div>
|
|
488
489
|
</>)}
|
|
489
490
|
|
|
490
491
|
{/* R-21:动作失败(拉取最新 / 浏览 / 下载 / 导入)已全部改由全局 Toast 送达,
|
|
@@ -216,7 +216,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
216
216
|
'list.searchPlaceholder': 'Search name / author / description…',
|
|
217
217
|
'list.categoriesAll': 'All categories',
|
|
218
218
|
'list.sectionsAll': 'All sections',
|
|
219
|
-
'list.sectionsUnknown': '{count} more item
|
|
219
|
+
'list.sectionsUnknown': '{count} more {count, plural, item|items} not downloaded yet — cannot match by section',
|
|
220
220
|
'list.sourceAll': 'All sources',
|
|
221
221
|
'list.sourceOfficial': 'Official',
|
|
222
222
|
'list.sourcePersonal': 'Community',
|
|
@@ -226,7 +226,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
226
226
|
'list.sortName': 'Name A–Z',
|
|
227
227
|
'list.stars': '⭐ {count}',
|
|
228
228
|
'list.starsHint': 'Stars of the source repo (repo-level, not per item)',
|
|
229
|
-
'list.count': '{count} item
|
|
229
|
+
'list.count': '{count} {count, plural, item|items}',
|
|
230
230
|
'list.filtered': '({count} after filter)',
|
|
231
231
|
'list.cacheCached': 'cached',
|
|
232
232
|
'list.cacheFresh': 'just fetched',
|
|
@@ -238,11 +238,11 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
238
238
|
'detail.emptySections': 'This item contains no importable sections',
|
|
239
239
|
'detail.needReview': 'Needs human confirmation',
|
|
240
240
|
'detail.previewHint': 'Read-only preview — nothing changed yet',
|
|
241
|
-
'detail.impact.willChange': '{count} item
|
|
241
|
+
'detail.impact.willChange': '{count} {count, plural, item|items} will change',
|
|
242
242
|
'detail.impact.unchanged': '{count} identical',
|
|
243
|
-
'detail.impact.conflicts': '{count} conflict
|
|
244
|
-
'detail.impact.secrets': '{count} secret
|
|
245
|
-
'detail.impact.paths': '{count} path mapping
|
|
243
|
+
'detail.impact.conflicts': '{count} {count, plural, conflict|conflicts}',
|
|
244
|
+
'detail.impact.secrets': '{count} {count, plural, secret|secrets} to re-enter',
|
|
245
|
+
'detail.impact.paths': '{count} path {count, plural, mapping|mappings}',
|
|
246
246
|
'detail.impact.restart': 'DSH restart required',
|
|
247
247
|
'detail.import': 'Confirm import',
|
|
248
248
|
'detail.back': 'Back to list',
|
|
@@ -250,14 +250,14 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
250
250
|
// semantics — cascade tree, select-all by default, "Select all" includes high-risk sections;
|
|
251
251
|
// risk is carried by the inline warning + disclaimer + pre-import snapshot + rollback).
|
|
252
252
|
'review.highRiskHint': 'High-risk sections selected ({sections}): an unreviewed public-repo item that installs plugins / writes files / injects global instructions on this machine.',
|
|
253
|
-
'review.changeCount': '{selected}/{total} item
|
|
253
|
+
'review.changeCount': '{selected}/{total} {total, plural, item|items} selected',
|
|
254
254
|
'review.changeWillChange': '{count} will change',
|
|
255
255
|
'review.changeUnchanged': '{count} identical',
|
|
256
256
|
'review.changeEmpty': 'This item has no selectable content items.',
|
|
257
257
|
'review.rowWillChange': 'Will change',
|
|
258
258
|
'review.rowUnchanged': 'Identical',
|
|
259
259
|
'review.rowExcluded': 'Not imported',
|
|
260
|
-
'review.conflictHint': '{count} conflict
|
|
260
|
+
'review.conflictHint': '{count} {count, plural, conflict needs|conflicts need} a decision; undecided ones keep local content.',
|
|
261
261
|
'review.nothingSelected': 'Nothing selected — cannot import',
|
|
262
262
|
'review.rollback': 'Roll back to pre-import snapshot',
|
|
263
263
|
'review.rollbackHint': 'Restores this machine to its state before the import.',
|
|
@@ -267,8 +267,8 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
267
267
|
'review.rollbackConfirm': 'Roll back now',
|
|
268
268
|
'review.rollbackDone': 'Rolled back to the pre-import snapshot',
|
|
269
269
|
'review.rollbackReport': 'Rollback report',
|
|
270
|
-
'review.rollbackRestored': '{count} item
|
|
271
|
-
'review.rollbackRemovedPlugins': '{count} plugin
|
|
270
|
+
'review.rollbackRestored': '{count} {count, plural, item|items} restored',
|
|
271
|
+
'review.rollbackRemovedPlugins': '{count} {count, plural, plugin|plugins} removed',
|
|
272
272
|
'review.rollbackFailed': '{count} failed',
|
|
273
273
|
'review.rollbackManual': '{count} need manual handling',
|
|
274
274
|
// Stepwise flow (P1): preview → select → (conflicts) → confirm → result.
|
|
@@ -284,16 +284,16 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
284
284
|
'review.filter.highRisk': 'High risk',
|
|
285
285
|
'review.filter.unselected': 'Not selected',
|
|
286
286
|
'review.filterEmpty': 'No items match the current filter.',
|
|
287
|
-
'review.sectionUnits': '{selected}/{total} item
|
|
287
|
+
'review.sectionUnits': '{selected}/{total} {total, plural, item|items} selected',
|
|
288
288
|
'review.excludedCount': '{count} excluded',
|
|
289
289
|
'review.next': 'Next',
|
|
290
290
|
'review.back': 'Back',
|
|
291
291
|
'review.confirmHint': 'A rollback snapshot is taken before importing',
|
|
292
|
-
'review.resultOk': '{count} item
|
|
293
|
-
'review.resultFailed': '{count} item
|
|
292
|
+
'review.resultOk': '{count} {count, plural, item|items} written',
|
|
293
|
+
'review.resultFailed': '{count} {count, plural, item|items} failed',
|
|
294
294
|
'review.reviewAgain': 'Back to selection',
|
|
295
|
-
'import.done': 'Import complete: {count} item
|
|
296
|
-
'import.failed': 'Import failed ({count} item
|
|
295
|
+
'import.done': 'Import complete: {count} {count, plural, item|items} written',
|
|
296
|
+
'import.failed': 'Import failed ({count} {count, plural, item|items} failed)',
|
|
297
297
|
'import.needsRestart': 'Some changes need a DSH restart to take effect',
|
|
298
298
|
'detail.failed': 'Download failed — see the notice at the bottom right',
|
|
299
299
|
'myconfigs.tab.browse': 'Browse Market',
|
|
@@ -345,7 +345,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
345
345
|
'myconfigs.list.empty': 'No configs uploaded yet',
|
|
346
346
|
'myconfigs.list.loading': 'Loading…',
|
|
347
347
|
'myconfigs.list.refresh': 'Refresh',
|
|
348
|
-
'myconfigs.list.summary': '{total} item
|
|
348
|
+
'myconfigs.list.summary': '{total} {total, plural, item|items} · {listed} listed · {pending} pending · {none} not listed',
|
|
349
349
|
'myconfigs.list.openRepo': 'Open repo',
|
|
350
350
|
'myconfigs.item.update': 'Update',
|
|
351
351
|
'myconfigs.item.install': 'Install locally',
|
|
@@ -790,10 +790,12 @@ export function RollbackCard({ recoveryApi, t }: RollbackCardProps) {
|
|
|
790
790
|
<div className={css.hint}>{t(dyn(verdict.reasonKey))}</div>
|
|
791
791
|
)}
|
|
792
792
|
|
|
793
|
+
{/* 动作顺序与 MergeModal 同口径(DESIGN §9):**取消在左、主操作在右**。
|
|
794
|
+
以前危险操作在左、取消在右 —— 两个弹窗的「取消」位置相反,用户会点错。 */}
|
|
793
795
|
<div className={css.actionRow}>
|
|
794
|
-
<Button variant="danger" disabled={busy || !canRewind || selectionBlocked} loading={busy} onClick={() => { setConfirmOpen(true) }}>{t('rollback.rewind')}</Button>
|
|
795
|
-
<span className={css.statusSpacer} />
|
|
796
796
|
<Button disabled={busy} onClick={closePreview}>{t('common.cancel')}</Button>
|
|
797
|
+
<span className={css.statusSpacer} />
|
|
798
|
+
<Button variant="danger" disabled={busy || !canRewind || selectionBlocked} loading={busy} onClick={() => { setConfirmOpen(true) }}>{t('rollback.rewind')}</Button>
|
|
797
799
|
</div>
|
|
798
800
|
</>
|
|
799
801
|
)}
|
|
@@ -342,9 +342,11 @@ test('T18-F1④:createdAfter / result.counts 必须说「留点时并不存在
|
|
|
342
342
|
}
|
|
343
343
|
assert.deepEqual(offenders, [], '计数类文案不得再写「未记录」(那是旧判据:既有文件也会落进那个计数):' + offenders.join(', '))
|
|
344
344
|
assert.ok(zh['rollback.preview.createdAfter'].includes('留点时并不存在'), 'zh createdAfter 必须说「留点时并不存在」')
|
|
345
|
+
// 2026-11-01 起单复数由 `{n, plural, one|other}` 段承担 ⇒ 动词跟着进段,
|
|
346
|
+
// 模板里不再有连续的 "did not exist"(渲染后才有)。断言模板结构,不断言拼接结果。
|
|
345
347
|
assert.ok(
|
|
346
|
-
en['rollback.preview.createdAfter'].
|
|
347
|
-
'en createdAfter 必须说 did not exist at that point',
|
|
348
|
+
en['rollback.preview.createdAfter'].includes('{n, plural, item did|items did} not exist at that point'),
|
|
349
|
+
'en createdAfter 必须说 did not exist at that point(动词进 plural 段后仍是这句)',
|
|
348
350
|
)
|
|
349
351
|
assert.ok(zh['rollback.rewind.result.counts'].includes('之后新建'), 'zh 结果计数必须说清 leftovers 的口径')
|
|
350
352
|
assert.ok(en['rollback.rewind.result.counts'].includes('created afterwards'), 'en 结果计数必须说清 leftovers 的口径')
|