diff --git a/src/i18n/locales/de-DE.ts b/src/i18n/locales/de-DE.ts index 41f9e943..916ac5aa 100644 --- a/src/i18n/locales/de-DE.ts +++ b/src/i18n/locales/de-DE.ts @@ -1018,6 +1018,14 @@ export const deDE: LanguagePack = { '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.versionEmptyCompareHint': + 'Wählen Sie linke und rechte Programmdateien mit Durchsuchen (oder fügen Sie Pfade ein), dann auf Vergleichen klicken.', + 'ui.versionEmptyReadyHint': + 'Drücken Sie Enter oder klicken Sie auf Vergleichen, um diese Versionsressourcen zu vergleichen.', + 'ui.versionIdenticalHint': + 'Diese Programmdateien stimmen überein. Keine Versionsressourcen-Unterschiede anzuzeigen.', + 'ui.versionCompareWindowsOnly': + 'Das Lesen nativer Versionsressourcen ist nur unter Windows verfügbar. Sie können hier trotzdem Pfade wählen; Vergleich erklärt, falls diese Plattform sie nicht lesen kann.', '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 493aaa4c..0639c81b 100644 --- a/src/i18n/locales/en-US.ts +++ b/src/i18n/locales/en-US.ts @@ -979,6 +979,13 @@ export const enUS: LanguagePack = { '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.versionEmptyCompareHint': + 'Choose left and right program files with Browse (or paste paths), then click Compare.', + 'ui.versionEmptyReadyHint': 'Press Enter or click Compare to compare these version resources.', + 'ui.versionIdenticalHint': + 'These program files match. No version-resource differences to show.', + 'ui.versionCompareWindowsOnly': + 'Native version resource reading is only available on Windows. You can still choose paths here; compare will explain if this platform cannot read them.', '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 85e8671d..a17796af 100644 --- a/src/i18n/locales/es-ES.ts +++ b/src/i18n/locales/es-ES.ts @@ -1002,6 +1002,14 @@ export const esES: LanguagePack = { '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.versionEmptyCompareHint': + 'Elija los archivos de programa izquierdo y derecho con Examinar (o pegue rutas) y luego haga clic en Comparar.', + 'ui.versionEmptyReadyHint': + 'Pulse Intro o haga clic en Comparar para comparar estos recursos de versión.', + 'ui.versionIdenticalHint': + 'Estos archivos de programa coinciden. No hay diferencias de recursos de versión que mostrar.', + 'ui.versionCompareWindowsOnly': + 'La lectura nativa de recursos de versión solo está disponible en Windows. Aún puede elegir rutas aquí; la comparación explicará si esta plataforma no puede leerlas.', '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 609fd84e..be10cd77 100644 --- a/src/i18n/locales/fr-FR.ts +++ b/src/i18n/locales/fr-FR.ts @@ -1012,6 +1012,14 @@ export const frFR: LanguagePack = { '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.versionEmptyCompareHint': + 'Choisissez les fichiers programme gauche et droit avec Parcourir (ou collez des chemins), puis cliquez sur Comparer.', + 'ui.versionEmptyReadyHint': + 'Appuyez sur Entrée ou cliquez sur Comparer pour comparer ces ressources de version.', + 'ui.versionIdenticalHint': + 'Ces fichiers programme correspondent. Aucune différence de ressources de version à afficher.', + 'ui.versionCompareWindowsOnly': + "La lecture native des ressources de version n'est disponible que sous Windows. Vous pouvez quand même choisir des chemins ici ; la comparaison expliquera si cette plate-forme ne peut pas les lire.", '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 546f4f93..441636f5 100644 --- a/src/i18n/locales/ja-JP.ts +++ b/src/i18n/locales/ja-JP.ts @@ -976,6 +976,13 @@ export const jaJP: LanguagePack = { '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.versionEmptyCompareHint': + 'Choose left and right program files with Browse (or paste paths), then click Compare.', + 'ui.versionEmptyReadyHint': 'Press Enter or click Compare to compare these version resources.', + 'ui.versionIdenticalHint': + 'These program files match. No version-resource differences to show.', + 'ui.versionCompareWindowsOnly': + 'Native version resource reading is only available on Windows. You can still choose paths here; compare will explain if this platform cannot read them.', '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 a50cf216..cf845be2 100644 --- a/src/i18n/locales/ko-KR.ts +++ b/src/i18n/locales/ko-KR.ts @@ -971,6 +971,12 @@ export const koKR: LanguagePack = { '이 레지스트리 내보내기는 일치합니다. 표시할 키 또는 값 차이가 없습니다.', 'ui.registryNeedsBothExports': '먼저 좌우 .reg 내보내기를 모두 붙여넣으세요.', 'ui.registryExportPlaceholder': '.reg 내보내기를 여기에 붙여넣으세요', + 'ui.versionEmptyCompareHint': + '찾아보기로 좌우 프로그램 파일을 선택하거나 경로를 붙여넣은 다음 비교를 클릭하세요.', + 'ui.versionEmptyReadyHint': 'Enter를 누르거나 비교를 클릭하여 이 버전 리소스를 비교하세요.', + 'ui.versionIdenticalHint': '이 프로그램 파일은 일치합니다. 표시할 버전 리소스 차이가 없습니다.', + 'ui.versionCompareWindowsOnly': + '네이티브 버전 리소스 읽기는 Windows에서만 사용할 수 있습니다. 여기서 경로를 선택할 수는 있으며, 이 플랫폼에서 읽을 수 없으면 비교 시 안내합니다.', 'ui.textPatchSourceTextPlaceholder': '패치 적용을 위해 원본 파일 텍스트를 붙여넣으세요', 'ui.applyPatchHint': '메모리에서 패치된 텍스트를 미리 봅니다(파일에 쓰지 않음).', 'ui.applyToFileHint': '패치 결과를 대상 파일에 씁니다(대상이 비어 있으면 원본에 씀).', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 784fdfc9..a95fa59d 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -931,6 +931,11 @@ export const zhCN: LanguagePack = { 'ui.registryIdenticalHint': '这些注册表导出一致。没有键或值差异。', 'ui.registryNeedsBothExports': '请先粘贴左右两侧的 .reg 导出。', 'ui.registryExportPlaceholder': '在此粘贴 .reg 导出', + 'ui.versionEmptyCompareHint': '用浏览选择左右程序文件(或粘贴路径),然后点比较。', + 'ui.versionEmptyReadyHint': '按 Enter 或点比较以比较这些版本资源。', + 'ui.versionIdenticalHint': '这些程序文件一致。没有版本资源差异。', + 'ui.versionCompareWindowsOnly': + '本机版本资源读取仅在 Windows 上可用。仍可在此选择路径;若当前平台无法读取,比较会说明原因。', 'ui.textPatchSourceTextPlaceholder': '粘贴原始文件文本以应用补丁', 'ui.applyPatchHint': '在内存中预览修补后的文本(不写入文件)。', 'ui.applyToFileHint': '将修补结果写入目标文件(若目标为空则写入源文件)。', diff --git a/src/i18n/locales/zh-TW.ts b/src/i18n/locales/zh-TW.ts index d54c6610..d3b4c3d0 100644 --- a/src/i18n/locales/zh-TW.ts +++ b/src/i18n/locales/zh-TW.ts @@ -933,6 +933,11 @@ export const zhTW: LanguagePack = { 'ui.registryIdenticalHint': '這些登錄檔匯出一致。沒有機碼或值差異。', 'ui.registryNeedsBothExports': '請先貼上左右兩側的 .reg 匯出。', 'ui.registryExportPlaceholder': '在此貼上 .reg 匯出', + 'ui.versionEmptyCompareHint': '用瀏覽選擇左右程式檔案(或貼上路徑),然後按比較。', + 'ui.versionEmptyReadyHint': '按 Enter 或按比較以比較這些版本資源。', + 'ui.versionIdenticalHint': '這些程式檔案一致。沒有版本資源差異。', + 'ui.versionCompareWindowsOnly': + '原生版本資源讀取僅在 Windows 上可用。仍可在此選擇路徑;若目前平台無法讀取,比較會說明原因。', 'ui.textPatchSourceTextPlaceholder': '貼上原始檔案文字以套用修補', 'ui.applyPatchHint': '在記憶體中預覽修補後的文字(不寫入檔案)。', 'ui.applyToFileHint': '將修補結果寫入目標檔案(若目標為空則寫入來源)。', diff --git a/src/views/VersionCompareView.test.ts b/src/views/VersionCompareView.test.ts index 7305ed81..1b124d36 100644 --- a/src/views/VersionCompareView.test.ts +++ b/src/views/VersionCompareView.test.ts @@ -1,6 +1,7 @@ import { flushPromises, mount } from '@vue/test-utils' import { createPinia, setActivePinia } from 'pinia' import { useSettingsStore } from '@/stores/settings' +import { usePolicyStore } from '@/stores/policy' import { beforeEach, describe, expect, it, vi } from 'vitest' import VersionCompareView from './VersionCompareView.vue' import { compareVersionFiles, saveTextFile } from '@/api/diff' @@ -142,6 +143,112 @@ describe('VersionCompareView', () => { expect( wrapper.find('[data-testid="version-session-toolbar-rules"]').attributes('disabled'), ).toBeUndefined() + expect(wrapper.find('[data-testid="version-empty-hint"]').text()).toContain('Browse') + expect(wrapper.find('[data-testid="version-left-path"]').attributes('placeholder')).toContain( + 'Browse or paste a file path', + ) + expect(wrapper.find('[data-testid="run-version-compare"]').attributes('title')).toContain( + 'left and right files first', + ) + expect(wrapper.find('[data-testid="run-version-compare"]').attributes('disabled')).toBeDefined() + expect(wrapper.find('[data-testid="version-windows-only"]').exists()).toBe(true) + }) + + it('shows a ready hint after both paths are set', async () => { + const wrapper = mount(VersionCompareView) + + await wrapper.find('[data-testid="version-left-path"]').setValue('C:/apps/left.exe') + await wrapper.find('[data-testid="version-right-path"]').setValue('C:/apps/right.exe') + await wrapper.vm.$nextTick() + + expect(wrapper.find('[data-testid="version-empty-hint"]').text()).toContain('Compare') + expect( + wrapper.find('[data-testid="run-version-compare"]').attributes('disabled'), + ).toBeUndefined() + }) + + it('compares version files on Enter when both paths are set', async () => { + const wrapper = mount(VersionCompareView) + + await wrapper.find('[data-testid="version-left-path"]').setValue('C:/apps/left.exe') + await wrapper.find('[data-testid="version-right-path"]').setValue('C:/apps/right.exe') + await wrapper.find('[data-testid="version-right-path"]').trigger('keydown.enter') + await flushPromises() + + expect(compareVersionFiles).toHaveBeenCalledWith({ + leftPath: 'C:/apps/left.exe', + rightPath: 'C:/apps/right.exe', + }) + expect(wrapper.find('[data-testid="version-empty-hint"]').exists()).toBe(false) + }) + + it('shows an identical hint when version resources match', async () => { + vi.mocked(compareVersionFiles).mockResolvedValueOnce({ + left: { + name: 'same.exe', + fileType: 'Application', + targetOs: 'Windows 32-bit', + fileVersion: '1.0.0.0', + productVersion: '1.0.0.0', + }, + right: { + name: 'same.exe', + fileType: 'Application', + targetOs: 'Windows 32-bit', + fileVersion: '1.0.0.0', + productVersion: '1.0.0.0', + }, + fields: [ + { + group: 'Fixed Info', + field: 'FileVersion', + left: '1.0.0.0', + right: '1.0.0.0', + status: 'unchanged', + }, + ], + summary: { added: 0, removed: 0, modified: 0, unchanged: 1 }, + }) + + const wrapper = mount(VersionCompareView) + + await wrapper.find('[data-testid="version-left-path"]').setValue('C:/apps/a.exe') + await wrapper.find('[data-testid="version-right-path"]').setValue('C:/apps/b.exe') + await wrapper.find('[data-testid="run-version-compare"]').trigger('click') + await flushPromises() + + expect(wrapper.find('[data-testid="version-identical-hint"]').text()).toContain('match') + }) + + it('maps unsupported-platform failures to a plain suggestion', async () => { + vi.mocked(compareVersionFiles).mockRejectedValueOnce({ + code: 'app.unknown', + messageKey: 'error.version.unsupportedPlatform.message', + suggestionKey: 'error.version.unsupportedPlatform.suggestion', + params: {}, + debugMessage: 'native version resource reading is only available on Windows', + }) + + const wrapper = mount(VersionCompareView) + + await wrapper.find('[data-testid="version-left-path"]').setValue('C:/apps/a.exe') + await wrapper.find('[data-testid="version-right-path"]').setValue('C:/apps/b.exe') + await wrapper.find('[data-testid="run-version-compare"]').trigger('click') + await flushPromises() + + expect(wrapper.find('[data-testid="version-compare-error"]').text()).toContain( + 'only available on Windows', + ) + }) + + it('hides the Windows-only note when running on Windows', () => { + const policy = usePolicyStore() + + policy.os = 'windows' + + const wrapper = mount(VersionCompareView) + + expect(wrapper.find('[data-testid="version-windows-only"]').exists()).toBe(false) }) it('filters minor differences and toggles importance rules', async () => { diff --git a/src/views/VersionCompareView.vue b/src/views/VersionCompareView.vue index b1598581..8d04f95e 100644 --- a/src/views/VersionCompareView.vue +++ b/src/views/VersionCompareView.vue @@ -35,8 +35,10 @@ import { useSessionLaunchStore } from '@/stores/sessionLaunch' import { useViewActionsStore } from '@/stores/viewActions' import { useTabsStore } from '@/stores/tabs' import { useSettingsStore } from '@/stores/settings' +import { usePolicyStore } from '@/stores/policy' const settings = useSettingsStore() +const policy = usePolicyStore() const versionStatuses: VersionFieldStatus[] = ['added', 'removed', 'modified', 'unchanged'] const emptyVersionSide: VersionSideSummary = { @@ -156,6 +158,32 @@ const minorDifferenceCount = computed( ).length, ) +const compareNeedsBothFiles = computed(() => !leftPath.value.trim() || !rightPath.value.trim()) +const hasVersionResult = computed(() => versionFields.value.length > 0) +const versionsAreIdentical = computed(() => { + if (!hasVersionResult.value) { + return false + } + + const summary = versionSummary.value + + return summary.added === 0 && summary.removed === 0 && summary.modified === 0 +}) +const versionEmptyStateMessage = computed(() => { + if (hasVersionResult.value) { + return '' + } + + if (leftPath.value.trim() && rightPath.value.trim()) { + return t('ui.versionEmptyReadyHint') + } + + return t('ui.versionEmptyCompareHint') +}) +const compareVersionTip = computed(() => + compareNeedsBothFiles.value ? t('ui.compareNeedsBothFiles') : t('ui.runDiff'), +) + const versionSessionToolbar = computed(() => buildVersionCompareToolbar({ home: true, @@ -544,22 +572,70 @@ async function exportVersionReport(): Promise { } } +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 formatVersionCompareError(event: unknown): string { + if (event && typeof event === 'object') { + const record = event as Record + const messageKey = readErrorStringField(record, 'messageKey', 'message_key') + + if (messageKey.startsWith('error.version.')) { + 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 onVersionPathEnter(): void { + if (compareNeedsBothFiles.value || loading.value) { + return + } + + void runVersionCompare() +} + async function runVersionCompare(): Promise { + if (compareNeedsBothFiles.value) { + return + } + const startedAt = performance.now() loading.value = true error.value = '' try { const result = await compareVersionFiles({ - leftPath: leftPath.value, - rightPath: rightPath.value, + leftPath: leftPath.value.trim(), + rightPath: rightPath.value.trim(), }) applyVersionResult(result) loadTimeSeconds.value = elapsedSecondsSince(startedAt) await refreshVersionPathStamps() } catch (event) { - error.value = formatCompareError(event, t) + error.value = formatVersionCompareError(event) await refreshVersionPathStamps() } finally { loading.value = false @@ -629,7 +705,9 @@ watch( type="text" class="path-input" data-testid="version-left-path" - :title="leftPath" + :title="leftPath || $t('ui.remoteUriHint')" + :placeholder="$t('ui.textPathPlaceholder')" + @keydown.enter.prevent="onVersionPathEnter" /> {{ $t('ui.runDiff') }} @@ -691,6 +772,27 @@ watch( > {{ error }}

+

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

+

+ {{ versionEmptyStateMessage }} +

+

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