diff --git a/src/i18n/locales/de-DE.ts b/src/i18n/locales/de-DE.ts index 42f307ca..41f9e943 100644 --- a/src/i18n/locales/de-DE.ts +++ b/src/i18n/locales/de-DE.ts @@ -1010,6 +1010,14 @@ export const deDE: LanguagePack = { 'Drücken Sie Enter oder klicken Sie auf Vergleichen, um diese Mediendateien zu vergleichen.', 'ui.mediaIdenticalHint': 'Diese Mediendateien stimmen überein. Keine Metadatenunterschiede anzuzeigen.', + 'ui.registryEmptyCompareHint': + 'Fügen Sie linke und rechte .reg-Exporte ein (oder vergleichen Sie unten Hive-Dateien / Live-Schlüssel), dann auf Vergleichen klicken.', + 'ui.registryEmptyReadyHint': + 'Drücken Sie Strg+Enter oder klicken Sie auf Vergleichen, um diese Registry-Exporte zu vergleichen.', + 'ui.registryIdenticalHint': + 'Diese Registry-Exporte stimmen überein. Keine Schlüssel- oder Wertunterschiede anzuzeigen.', + 'ui.registryNeedsBothExports': 'Fügen Sie zuerst beide .reg-Exporte (links und rechts) ein.', + 'ui.registryExportPlaceholder': 'Hier einen .reg-Export einfügen', 'ui.textPatchSourceTextPlaceholder': 'Originaltextdatei für „Patch anwenden“ einfügen', 'ui.applyPatchHint': 'Zeigt den gepatchten Text im Speicher an (schreibt keine Datei).', 'ui.applyToFileHint': 'Schreibt das Ergebnis in die Zieldatei (oder Quelle, wenn Ziel leer).', diff --git a/src/i18n/locales/en-US.ts b/src/i18n/locales/en-US.ts index d7c5723e..493aaa4c 100644 --- a/src/i18n/locales/en-US.ts +++ b/src/i18n/locales/en-US.ts @@ -971,6 +971,14 @@ export const enUS: LanguagePack = { 'Choose left and right media files with Browse (or paste paths), then click Compare.', 'ui.mediaEmptyReadyHint': 'Press Enter or click Compare to compare these media files.', 'ui.mediaIdenticalHint': 'These media files match. No metadata differences to show.', + 'ui.registryEmptyCompareHint': + 'Paste left and right .reg exports (or compare hive files / live keys below), then click Compare.', + 'ui.registryEmptyReadyHint': + 'Press Ctrl+Enter or click Compare to compare these registry exports.', + 'ui.registryIdenticalHint': + 'These registry exports match. No key or value differences to show.', + 'ui.registryNeedsBothExports': 'Paste both left and right .reg exports first.', + 'ui.registryExportPlaceholder': 'Paste a .reg export here', 'ui.textPatchSourceTextPlaceholder': 'Paste the original file text for Apply Patch', 'ui.applyPatchHint': 'Preview the patched text in memory (does not write a file).', 'ui.applyToFileHint': diff --git a/src/i18n/locales/es-ES.ts b/src/i18n/locales/es-ES.ts index 9ec9620b..85e8671d 100644 --- a/src/i18n/locales/es-ES.ts +++ b/src/i18n/locales/es-ES.ts @@ -994,6 +994,14 @@ export const esES: LanguagePack = { 'Pulse Intro o haga clic en Comparar para comparar estos archivos multimedia.', 'ui.mediaIdenticalHint': 'Estos archivos multimedia coinciden. No hay diferencias de metadatos que mostrar.', + 'ui.registryEmptyCompareHint': + 'Pegue exportaciones .reg izquierda y derecha (o compare archivos hive / claves en vivo abajo) y luego haga clic en Comparar.', + 'ui.registryEmptyReadyHint': + 'Pulse Ctrl+Intro o haga clic en Comparar para comparar estas exportaciones del registro.', + 'ui.registryIdenticalHint': + 'Estas exportaciones del registro coinciden. No hay diferencias de claves o valores que mostrar.', + 'ui.registryNeedsBothExports': 'Pegue primero ambas exportaciones .reg (izquierda y derecha).', + 'ui.registryExportPlaceholder': 'Pegue aquí una exportación .reg', 'ui.textPatchSourceTextPlaceholder': 'Pegue el texto del archivo original para Aplicar parche', 'ui.applyPatchHint': 'Previsualiza el texto parcheado en memoria (no escribe un archivo).', 'ui.applyToFileHint': diff --git a/src/i18n/locales/fr-FR.ts b/src/i18n/locales/fr-FR.ts index 9367000f..609fd84e 100644 --- a/src/i18n/locales/fr-FR.ts +++ b/src/i18n/locales/fr-FR.ts @@ -1004,6 +1004,14 @@ export const frFR: LanguagePack = { 'Appuyez sur Entrée ou cliquez sur Comparer pour comparer ces fichiers multimédias.', 'ui.mediaIdenticalHint': 'Ces fichiers multimédias correspondent. Aucune différence de métadonnées à afficher.', + 'ui.registryEmptyCompareHint': + 'Collez les exports .reg gauche et droit (ou comparez les ruches / clés en direct ci-dessous), puis cliquez sur Comparer.', + 'ui.registryEmptyReadyHint': + 'Appuyez sur Ctrl+Entrée ou cliquez sur Comparer pour comparer ces exports du Registre.', + 'ui.registryIdenticalHint': + 'Ces exports du Registre correspondent. Aucune différence de clé ou de valeur à afficher.', + 'ui.registryNeedsBothExports': "Collez d'abord les deux exports .reg (gauche et droit).", + 'ui.registryExportPlaceholder': 'Collez un export .reg ici', 'ui.textPatchSourceTextPlaceholder': "Collez le texte du fichier d'origine pour Appliquer le correctif", 'ui.applyPatchHint': "Prévisualise le texte corrigé en mémoire (n'écrit pas de fichier).", diff --git a/src/i18n/locales/ja-JP.ts b/src/i18n/locales/ja-JP.ts index c9139d51..546f4f93 100644 --- a/src/i18n/locales/ja-JP.ts +++ b/src/i18n/locales/ja-JP.ts @@ -968,6 +968,14 @@ export const jaJP: LanguagePack = { 'Choose left and right media files with Browse (or paste paths), then click Compare.', 'ui.mediaEmptyReadyHint': 'Press Enter or click Compare to compare these media files.', 'ui.mediaIdenticalHint': 'These media files match. No metadata differences to show.', + 'ui.registryEmptyCompareHint': + 'Paste left and right .reg exports (or compare hive files / live keys below), then click Compare.', + 'ui.registryEmptyReadyHint': + 'Press Ctrl+Enter or click Compare to compare these registry exports.', + 'ui.registryIdenticalHint': + 'These registry exports match. No key or value differences to show.', + 'ui.registryNeedsBothExports': 'Paste both left and right .reg exports first.', + 'ui.registryExportPlaceholder': 'Paste a .reg export here', 'ui.textPatchSourceTextPlaceholder': 'Paste the original file text for Apply Patch', 'ui.applyPatchHint': 'Preview the patched text in memory (does not write a file).', 'ui.applyToFileHint': diff --git a/src/i18n/locales/ko-KR.ts b/src/i18n/locales/ko-KR.ts index da6f462e..a50cf216 100644 --- a/src/i18n/locales/ko-KR.ts +++ b/src/i18n/locales/ko-KR.ts @@ -963,6 +963,14 @@ export const koKR: LanguagePack = { '찾아보기로 좌우 미디어 파일을 선택하거나 경로를 붙여넣은 다음 비교를 클릭하세요.', 'ui.mediaEmptyReadyHint': 'Enter를 누르거나 비교를 클릭하여 이 미디어 파일을 비교하세요.', 'ui.mediaIdenticalHint': '이 미디어 파일은 일치합니다. 표시할 메타데이터 차이가 없습니다.', + 'ui.registryEmptyCompareHint': + '좌우 .reg 내보내기를 붙여넣거나 아래에서 하이브/실시간 키를 비교한 다음 비교를 클릭하세요.', + 'ui.registryEmptyReadyHint': + 'Ctrl+Enter를 누르거나 비교를 클릭하여 이 레지스트리 내보내기를 비교하세요.', + 'ui.registryIdenticalHint': + '이 레지스트리 내보내기는 일치합니다. 표시할 키 또는 값 차이가 없습니다.', + 'ui.registryNeedsBothExports': '먼저 좌우 .reg 내보내기를 모두 붙여넣으세요.', + 'ui.registryExportPlaceholder': '.reg 내보내기를 여기에 붙여넣으세요', 'ui.textPatchSourceTextPlaceholder': '패치 적용을 위해 원본 파일 텍스트를 붙여넣으세요', 'ui.applyPatchHint': '메모리에서 패치된 텍스트를 미리 봅니다(파일에 쓰지 않음).', 'ui.applyToFileHint': '패치 결과를 대상 파일에 씁니다(대상이 비어 있으면 원본에 씀).', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 0e6facd6..784fdfc9 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -925,6 +925,12 @@ export const zhCN: LanguagePack = { 'ui.mediaEmptyCompareHint': '用浏览选择左右媒体文件(或粘贴路径),然后点比较。', 'ui.mediaEmptyReadyHint': '按 Enter 或点比较以比较这些媒体文件。', 'ui.mediaIdenticalHint': '这些媒体文件一致。没有元数据差异。', + 'ui.registryEmptyCompareHint': + '粘贴左右 .reg 导出(或在下方比较配置单元/实时键),然后点比较。', + 'ui.registryEmptyReadyHint': '按 Ctrl+Enter 或点比较以比较这些注册表导出。', + 'ui.registryIdenticalHint': '这些注册表导出一致。没有键或值差异。', + 'ui.registryNeedsBothExports': '请先粘贴左右两侧的 .reg 导出。', + 'ui.registryExportPlaceholder': '在此粘贴 .reg 导出', 'ui.textPatchSourceTextPlaceholder': '粘贴原始文件文本以应用补丁', 'ui.applyPatchHint': '在内存中预览修补后的文本(不写入文件)。', 'ui.applyToFileHint': '将修补结果写入目标文件(若目标为空则写入源文件)。', diff --git a/src/i18n/locales/zh-TW.ts b/src/i18n/locales/zh-TW.ts index def2536f..d54c6610 100644 --- a/src/i18n/locales/zh-TW.ts +++ b/src/i18n/locales/zh-TW.ts @@ -927,6 +927,12 @@ export const zhTW: LanguagePack = { 'ui.mediaEmptyCompareHint': '用瀏覽選擇左右媒體檔案(或貼上路徑),然後按比較。', 'ui.mediaEmptyReadyHint': '按 Enter 或按比較以比較這些媒體檔案。', 'ui.mediaIdenticalHint': '這些媒體檔案一致。沒有中繼資料差異。', + 'ui.registryEmptyCompareHint': + '貼上左右 .reg 匯出(或在下方比較配置單元/即時鍵),然後按比較。', + 'ui.registryEmptyReadyHint': '按 Ctrl+Enter 或按比較以比較這些登錄檔匯出。', + 'ui.registryIdenticalHint': '這些登錄檔匯出一致。沒有機碼或值差異。', + 'ui.registryNeedsBothExports': '請先貼上左右兩側的 .reg 匯出。', + 'ui.registryExportPlaceholder': '在此貼上 .reg 匯出', 'ui.textPatchSourceTextPlaceholder': '貼上原始檔案文字以套用修補', 'ui.applyPatchHint': '在記憶體中預覽修補後的文字(不寫入檔案)。', 'ui.applyToFileHint': '將修補結果寫入目標檔案(若目標為空則寫入來源)。', diff --git a/src/views/RegistryCompareView.test.ts b/src/views/RegistryCompareView.test.ts index ad145025..70b16cdd 100644 --- a/src/views/RegistryCompareView.test.ts +++ b/src/views/RegistryCompareView.test.ts @@ -187,6 +187,99 @@ describe('RegistryCompareView', () => { expect(wrapper.find('[data-testid="registry-summary-modified"]').text()).toContain('0') expect(wrapper.find('[data-testid="registry-key-HKCU/Software/OpenDiff"]').exists()).toBe(false) expect(wrapper.find('[data-testid="registry-maturity-note"]').exists()).toBe(true) + expect(wrapper.find('[data-testid="registry-empty-hint"]').text()).toContain('.reg') + expect( + wrapper.find('[data-testid="registry-left-export"]').attributes('placeholder'), + ).toContain('.reg') + expect(wrapper.find('[data-testid="run-registry-compare"]').attributes('title')).toContain( + 'both left and right', + ) + expect( + wrapper.find('[data-testid="run-registry-compare"]').attributes('disabled'), + ).toBeDefined() + expect( + wrapper.find('[data-testid="registry-hive-left-path"]').attributes('placeholder'), + ).toContain('Browse or paste a file path') + }) + + it('shows a ready hint after both exports are pasted', async () => { + const wrapper = mount(RegistryCompareView) + + await wrapper.find('[data-testid="registry-left-export"]').setValue('Windows Registry Editor') + await wrapper.find('[data-testid="registry-right-export"]').setValue('Windows Registry Editor') + await wrapper.vm.$nextTick() + + expect(wrapper.find('[data-testid="registry-empty-hint"]').text()).toContain('Compare') + expect( + wrapper.find('[data-testid="run-registry-compare"]').attributes('disabled'), + ).toBeUndefined() + }) + + it('compares registry exports on Ctrl+Enter', async () => { + const wrapper = mount(RegistryCompareView) + + await wrapper.find('[data-testid="registry-left-export"]').setValue('left export') + await wrapper.find('[data-testid="registry-right-export"]').setValue('right export') + await wrapper.find('[data-testid="registry-right-export"]').trigger('keydown.ctrl.enter') + await flushPromises() + + expect(compareRegistryExports).toHaveBeenCalled() + expect(wrapper.find('[data-testid="registry-empty-hint"]').exists()).toBe(false) + }) + + it('shows an identical hint when registry exports match', async () => { + vi.mocked(compareRegistryExports).mockResolvedValueOnce({ + leftName: 'same-left.reg', + rightName: 'same-right.reg', + tree: [ + { + path: 'HKCU/Software/OpenDiff', + label: 'OpenDiff', + status: 'unchanged', + values: [ + { + keyPath: 'HKCU/Software/OpenDiff', + name: 'Theme', + status: 'unchanged', + left: { kind: 'REG_SZ', data: 'dark' }, + right: { kind: 'REG_SZ', data: 'dark' }, + }, + ], + children: [], + }, + ], + summary: { added: 0, removed: 0, modified: 0, unchanged: 1 }, + }) + + const wrapper = mount(RegistryCompareView) + + await wrapper.find('[data-testid="registry-left-export"]').setValue('left export') + await wrapper.find('[data-testid="registry-right-export"]').setValue('right export') + await wrapper.find('[data-testid="run-registry-compare"]').trigger('click') + await flushPromises() + + expect(wrapper.find('[data-testid="registry-identical-hint"]').text()).toContain('match') + }) + + it('maps registry parse failures to a plain suggestion', async () => { + vi.mocked(compareRegistryExports).mockRejectedValueOnce({ + code: 'app.unknown', + messageKey: 'error.registry.parseFailed.message', + suggestionKey: 'error.registry.parseFailed.suggestion', + params: {}, + debugMessage: 'parse boom', + }) + + const wrapper = mount(RegistryCompareView) + + await wrapper.find('[data-testid="registry-left-export"]').setValue('bad') + await wrapper.find('[data-testid="registry-right-export"]').setValue('bad') + await wrapper.find('[data-testid="run-registry-compare"]').trigger('click') + await flushPromises() + + expect(wrapper.find('[data-testid="registry-compare-error"]').text()).toContain( + 'Could not parse', + ) }) it('filters diffs, applies right value in the workspace, and probes live query honesty', async () => { diff --git a/src/views/RegistryCompareView.vue b/src/views/RegistryCompareView.vue index 882cf295..2504390a 100644 --- a/src/views/RegistryCompareView.vue +++ b/src/views/RegistryCompareView.vue @@ -46,6 +46,7 @@ import { elapsedSecondsSince } from '@/app/statusBarPhrases' import { useViewActionsStore } from '@/stores/viewActions' import { useSettingsStore } from '@/stores/settings' import { useI18n } from '@/i18n' +import { formatCompareError } from '@/app/compareError' interface FlatRegistryKeyNode extends RegistryKeyNode { depth: number @@ -170,6 +171,91 @@ const selectedValue = computed(() => (value) => `${value.keyPath}::${value.name}` === selectedValueKey.value, ), ) +const compareNeedsBothExports = computed( + () => !leftExport.value.trim() || !rightExport.value.trim(), +) +const hasRegistryResult = computed(() => registryTree.value.length > 0) +const registryAreIdentical = computed(() => { + if (!hasRegistryResult.value) { + return false + } + + const summary = registrySummary.value + + return summary.added === 0 && summary.removed === 0 && summary.modified === 0 +}) +const registryEmptyStateMessage = computed(() => { + if (hasRegistryResult.value) { + return '' + } + + if (leftExport.value.trim() && rightExport.value.trim()) { + return t('ui.registryEmptyReadyHint') + } + + return t('ui.registryEmptyCompareHint') +}) +const compareExportsTip = computed(() => + compareNeedsBothExports.value ? t('ui.registryNeedsBothExports') : t('ui.runDiff'), +) +const compareNeedsBothHiveFiles = computed( + () => !leftHivePath.value.trim() || !rightHivePath.value.trim(), +) +const compareHiveTip = computed(() => + compareNeedsBothHiveFiles.value ? t('ui.compareNeedsBothFiles') : t('ui.compareHiveFiles'), +) + +function readErrorStringField(record: Record, ...keys: string[]): string { + for (const key of keys) { + const value = record[key] + + if (typeof value === 'string' && value.length > 0) { + return value + } + } + + return '' +} + +function formatRegistryCompareError(event: unknown): string { + if (event && typeof event === 'object') { + const record = event as Record + const messageKey = readErrorStringField(record, 'messageKey', 'message_key') + + if (messageKey.startsWith('error.registry.')) { + const params = + record.params && typeof record.params === 'object' + ? (record.params as Record) + : {} + const message = t(messageKey, params) + const suggestionKey = readErrorStringField(record, 'suggestionKey', 'suggestion_key') + + if (suggestionKey) { + return `${message} ${t(suggestionKey, params)}`.trim() + } + + return message + } + } + + return formatCompareError(event, t) +} + +function onRegistryExportCtrlEnter(): void { + if (compareNeedsBothExports.value || loading.value) { + return + } + + void runRegistryCompare() +} + +function onHivePathEnter(): void { + if (compareNeedsBothHiveFiles.value || hiveCompareLoading.value) { + return + } + + void runHiveFileCompare() +} function flattenRegistryKeys( nodes: RegistryKeyNode[], @@ -272,6 +358,10 @@ watchEffect(() => { }) async function runRegistryCompare(): Promise { + if (compareNeedsBothExports.value) { + return + } + const startedAt = performance.now() loading.value = true @@ -287,7 +377,7 @@ async function runRegistryCompare(): Promise { applyRegistryResult(result) loadTimeSeconds.value = elapsedSecondsSince(startedAt) } catch (event) { - error.value = String(event) + error.value = formatRegistryCompareError(event) } finally { loading.value = false } @@ -315,7 +405,7 @@ async function loadLaunchRegistryExports(leftPath: string, rightPath: string): P rightName.value = fileNameFromPath(rightFile.path) await runRegistryCompare() } catch (event) { - error.value = String(event) + error.value = formatRegistryCompareError(event) loading.value = false } } @@ -527,7 +617,7 @@ async function runLiveRegistryCompare(): Promise { applyRegistryResult(result) loadTimeSeconds.value = elapsedSecondsSince(startedAt) } catch (event) { - error.value = String(event) + error.value = formatRegistryCompareError(event) } finally { liveCompareLoading.value = false } @@ -559,7 +649,7 @@ async function runHiveFileCompare(): Promise { applyRegistryResult(result) loadTimeSeconds.value = elapsedSecondsSince(startedAt) } catch (event) { - error.value = String(event) + error.value = formatRegistryCompareError(event) } finally { hiveCompareLoading.value = false } @@ -606,7 +696,7 @@ async function exportRegistryReport(): Promise { }) reportStatus.value = outputPath } catch (event) { - error.value = String(event) + error.value = formatRegistryCompareError(event) } } @@ -929,10 +1019,18 @@ function runRegistryToolbarCommand(commandId: string): void {

+ {{ $t('ui.registryIdenticalHint') }} +

+

- {{ $t('ui.registryExportHint') }} + {{ registryEmptyStateMessage }}