From 60bf505ac86d6af47ba71917e094dfccc997a4c4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 24 Jan 2026 11:41:46 +0000
Subject: [PATCH 1/3] Initial plan
From c3f03c8340df196a47cd3efd1a57bf0d1385a038 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 24 Jan 2026 11:47:52 +0000
Subject: [PATCH 2/3] Add i18n support, rename layout buttons, fix artifact
workflow, add viewed checkbox feature
Co-authored-by: unknowIfGuestInDream <57802425+unknowIfGuestInDream@users.noreply.github.com>
---
.github/workflows/push-artifact.yml | 16 +---
src/common/_locales/en/messages.json | 44 +++++++++
src/common/_locales/ja/messages.json | 44 +++++++++
src/common/_locales/zh/messages.json | 44 +++++++++
src/common/diff-viewer.html | 19 ++--
src/common/diff-viewer.js | 115 ++++++++++++++++++++++-
src/common/i18n.js | 131 +++++++++++++++++++++++++++
src/common/styles.css | 56 ++++++++++++
8 files changed, 441 insertions(+), 28 deletions(-)
create mode 100644 src/common/_locales/en/messages.json
create mode 100644 src/common/_locales/ja/messages.json
create mode 100644 src/common/_locales/zh/messages.json
create mode 100644 src/common/i18n.js
diff --git a/.github/workflows/push-artifact.yml b/.github/workflows/push-artifact.yml
index 9f53ab1..8a40b9d 100644
--- a/.github/workflows/push-artifact.yml
+++ b/.github/workflows/push-artifact.yml
@@ -27,28 +27,16 @@ jobs:
- name: Build extensions
run: npm run build
- - name: Create Chrome extension zip
- run: |
- cd dist/chrome
- zip -r ../../patchReader-chrome.zip .
- cd ../..
-
- - name: Create Edge extension zip
- run: |
- cd dist/edge
- zip -r ../../patchReader-edge.zip .
- cd ../..
-
- name: Upload Chrome extension artifact
uses: actions/upload-artifact@v6
with:
name: patchReader-chrome
- path: patchReader-chrome.zip
+ path: dist/chrome/
retention-days: 7
- name: Upload Edge extension artifact
uses: actions/upload-artifact@v6
with:
name: patchReader-edge
- path: patchReader-edge.zip
+ path: dist/edge/
retention-days: 7
diff --git a/src/common/_locales/en/messages.json b/src/common/_locales/en/messages.json
new file mode 100644
index 0000000..2d3b747
--- /dev/null
+++ b/src/common/_locales/en/messages.json
@@ -0,0 +1,44 @@
+{
+ "extName": {
+ "message": "Patch Reader"
+ },
+ "extDescription": {
+ "message": "Read and render patch/diff files with diff2html"
+ },
+ "sideBySize": {
+ "message": "Side-by-Side"
+ },
+ "unified": {
+ "message": "Unified"
+ },
+ "inputPatchContent": {
+ "message": "Input Patch/Diff Content"
+ },
+ "uploadFile": {
+ "message": "Upload File"
+ },
+ "clear": {
+ "message": "Clear"
+ },
+ "render": {
+ "message": "Render"
+ },
+ "diffPreview": {
+ "message": "Diff Preview"
+ },
+ "placeholder": {
+ "message": "After entering or uploading patch/diff content, the rendered result will be displayed here"
+ },
+ "inputPlaceholder": {
+ "message": "Paste patch/diff content here, or click the upload button to upload .patch or .diff files..."
+ },
+ "fileReadFailed": {
+ "message": "File read failed"
+ },
+ "renderFailed": {
+ "message": "Render failed"
+ },
+ "viewed": {
+ "message": "Viewed"
+ }
+}
diff --git a/src/common/_locales/ja/messages.json b/src/common/_locales/ja/messages.json
new file mode 100644
index 0000000..f2301b5
--- /dev/null
+++ b/src/common/_locales/ja/messages.json
@@ -0,0 +1,44 @@
+{
+ "extName": {
+ "message": "Patch Reader"
+ },
+ "extDescription": {
+ "message": "diff2htmlでpatch/diffファイルを読み取り、レンダリングします"
+ },
+ "sideBySize": {
+ "message": "サイドバイサイド"
+ },
+ "unified": {
+ "message": "統合ビュー"
+ },
+ "inputPatchContent": {
+ "message": "Patch/Diff 内容を入力"
+ },
+ "uploadFile": {
+ "message": "ファイルをアップロード"
+ },
+ "clear": {
+ "message": "クリア"
+ },
+ "render": {
+ "message": "レンダリング"
+ },
+ "diffPreview": {
+ "message": "Diff プレビュー"
+ },
+ "placeholder": {
+ "message": "patch/diff 内容を入力またはアップロードすると、レンダリング結果がここに表示されます"
+ },
+ "inputPlaceholder": {
+ "message": "ここに patch/diff 内容を貼り付けるか、アップロードボタンをクリックして .patch または .diff ファイルをアップロードしてください..."
+ },
+ "fileReadFailed": {
+ "message": "ファイルの読み取りに失敗しました"
+ },
+ "renderFailed": {
+ "message": "レンダリングに失敗しました"
+ },
+ "viewed": {
+ "message": "確認済み"
+ }
+}
diff --git a/src/common/_locales/zh/messages.json b/src/common/_locales/zh/messages.json
new file mode 100644
index 0000000..f2aaee8
--- /dev/null
+++ b/src/common/_locales/zh/messages.json
@@ -0,0 +1,44 @@
+{
+ "extName": {
+ "message": "Patch Reader"
+ },
+ "extDescription": {
+ "message": "使用 diff2html 读取和渲染 patch/diff 文件"
+ },
+ "sideBySize": {
+ "message": "并排对比"
+ },
+ "unified": {
+ "message": "统一视图"
+ },
+ "inputPatchContent": {
+ "message": "输入 Patch/Diff 内容"
+ },
+ "uploadFile": {
+ "message": "上传文件"
+ },
+ "clear": {
+ "message": "清除"
+ },
+ "render": {
+ "message": "渲染"
+ },
+ "diffPreview": {
+ "message": "Diff 预览"
+ },
+ "placeholder": {
+ "message": "输入或上传 patch/diff 内容后,渲染结果将显示在这里"
+ },
+ "inputPlaceholder": {
+ "message": "在此粘贴 patch/diff 内容,或点击上传文件按钮上传 .patch 或 .diff 文件..."
+ },
+ "fileReadFailed": {
+ "message": "文件读取失败"
+ },
+ "renderFailed": {
+ "message": "渲染失败"
+ },
+ "viewed": {
+ "message": "已查看"
+ }
+}
diff --git a/src/common/diff-viewer.html b/src/common/diff-viewer.html
index fdf6e68..dcaba51 100644
--- a/src/common/diff-viewer.html
+++ b/src/common/diff-viewer.html
@@ -1,5 +1,5 @@
-
+
@@ -12,30 +12,30 @@
-
+
@@ -43,6 +43,7 @@
Diff 预览
+
diff --git a/src/common/diff-viewer.js b/src/common/diff-viewer.js
index 2049337..c1bbfce 100644
--- a/src/common/diff-viewer.js
+++ b/src/common/diff-viewer.js
@@ -14,6 +14,9 @@
// Current layout mode: 'side-by-side' or 'line-by-line'
let currentLayout = 'side-by-side';
+ // Track viewed files
+ let viewedFiles = new Set();
+
// Initialize
function init() {
bindEvents();
@@ -47,6 +50,32 @@
}
}, 100);
});
+
+ // Delegate click events for viewed checkboxes
+ diffOutput.addEventListener('change', handleViewedCheckboxChange);
+ }
+
+ // Handle viewed checkbox change
+ function handleViewedCheckboxChange(event) {
+ if (event.target.classList.contains('viewed-checkbox')) {
+ const fileHeader = event.target.closest('.d2h-file-header');
+ const fileWrapper = event.target.closest('.d2h-file-wrapper');
+ const fileId = event.target.dataset.fileId;
+
+ if (event.target.checked) {
+ viewedFiles.add(fileId);
+ if (fileWrapper) {
+ fileWrapper.classList.add('file-viewed');
+ }
+ } else {
+ viewedFiles.delete(fileId);
+ if (fileWrapper) {
+ fileWrapper.classList.remove('file-viewed');
+ }
+ }
+
+ saveViewedFiles();
+ }
}
// Load saved state from localStorage
@@ -62,6 +91,12 @@
diffInput.value = savedContent;
renderDiff();
}
+
+ // Load viewed files
+ const savedViewedFiles = localStorage.getItem('patchReader_viewedFiles');
+ if (savedViewedFiles) {
+ viewedFiles = new Set(JSON.parse(savedViewedFiles));
+ }
} catch (e) {
console.warn('Unable to load saved state:', e);
}
@@ -77,6 +112,15 @@
}
}
+ // Save viewed files to localStorage
+ function saveViewedFiles() {
+ try {
+ localStorage.setItem('patchReader_viewedFiles', JSON.stringify([...viewedFiles]));
+ } catch (e) {
+ console.warn('Unable to save viewed files:', e);
+ }
+ }
+
// Set layout mode
function setLayout(layout, rerender = true) {
currentLayout = layout;
@@ -127,23 +171,70 @@
}).join('\n\n');
diffInput.value = combinedContent;
+ // Clear viewed files when new content is loaded
+ viewedFiles.clear();
+ saveViewedFiles();
renderDiff();
saveState();
})
.catch(error => {
- showError(`文件读取失败: ${error.message}`);
+ const errorMsg = window.i18n ? window.i18n.getMessage('fileReadFailed') : 'File read failed';
+ showError(`${errorMsg}: ${error.message}`);
});
// Reset file input
fileInput.value = '';
}
+ // Add viewed checkbox to file headers
+ function addViewedCheckboxes() {
+ const fileHeaders = diffOutput.querySelectorAll('.d2h-file-header');
+ const viewedLabel = window.i18n ? window.i18n.getMessage('viewed') : 'Viewed';
+
+ fileHeaders.forEach((header, index) => {
+ // Check if checkbox already exists
+ if (header.querySelector('.viewed-checkbox-wrapper')) return;
+
+ // Get file name for identification
+ const fileNameElement = header.querySelector('.d2h-file-name');
+ const fileName = fileNameElement ? fileNameElement.textContent.trim() : `file-${index}`;
+ const fileId = `${fileName}-${index}`;
+
+ // Create checkbox wrapper
+ const wrapper = document.createElement('label');
+ wrapper.className = 'viewed-checkbox-wrapper';
+
+ const checkbox = document.createElement('input');
+ checkbox.type = 'checkbox';
+ checkbox.className = 'viewed-checkbox';
+ checkbox.dataset.fileId = fileId;
+ checkbox.checked = viewedFiles.has(fileId);
+
+ const label = document.createElement('span');
+ label.className = 'viewed-label';
+ label.textContent = viewedLabel;
+
+ wrapper.appendChild(checkbox);
+ wrapper.appendChild(label);
+
+ // Insert at the end of file header
+ header.appendChild(wrapper);
+
+ // Apply viewed state
+ const fileWrapper = header.closest('.d2h-file-wrapper');
+ if (checkbox.checked && fileWrapper) {
+ fileWrapper.classList.add('file-viewed');
+ }
+ });
+ }
+
// Render diff content
function renderDiff() {
const diffString = diffInput.value.trim();
if (!diffString) {
- diffOutput.innerHTML = '输入或上传 patch/diff 内容后,渲染结果将显示在这里
';
+ const placeholderMsg = window.i18n ? window.i18n.getMessage('placeholder') : 'After entering or uploading patch/diff content, the rendered result will be displayed here';
+ diffOutput.innerHTML = `${escapeHtml(placeholderMsg)}
`;
return;
}
@@ -162,23 +253,37 @@
matching: 'lines',
matchWordsThreshold: 0.25,
maxLineLengthHighlight: 10000,
- renderNothingWhenEmpty: false
+ renderNothingWhenEmpty: false,
+ fileListToggle: true,
+ fileListStartVisible: true,
+ fileContentToggle: true,
+ stickyFileHeaders: true
});
diffOutput.innerHTML = html;
+
+ // Add viewed checkboxes after rendering
+ addViewedCheckboxes();
+
saveState();
} catch (error) {
- showError(`渲染失败: ${error.message}`);
+ const errorMsg = window.i18n ? window.i18n.getMessage('renderFailed') : 'Render failed';
+ showError(`${errorMsg}: ${error.message}`);
}
}
// Clear all content
function clearAll() {
diffInput.value = '';
- diffOutput.innerHTML = '输入或上传 patch/diff 内容后,渲染结果将显示在这里
';
+ const placeholderMsg = window.i18n ? window.i18n.getMessage('placeholder') : 'After entering or uploading patch/diff content, the rendered result will be displayed here';
+ diffOutput.innerHTML = `${escapeHtml(placeholderMsg)}
`;
+
+ // Clear viewed files
+ viewedFiles.clear();
try {
localStorage.removeItem('patchReader_content');
+ localStorage.removeItem('patchReader_viewedFiles');
} catch (e) {
console.warn('Unable to clear saved content:', e);
}
diff --git a/src/common/i18n.js b/src/common/i18n.js
new file mode 100644
index 0000000..fabf7d0
--- /dev/null
+++ b/src/common/i18n.js
@@ -0,0 +1,131 @@
+// Internationalization (i18n) module
+(function() {
+ 'use strict';
+
+ const supportedLocales = ['en', 'zh', 'ja'];
+ const defaultLocale = 'en';
+ let currentLocale = defaultLocale;
+ let messages = {};
+
+ // Get browser language
+ function getBrowserLanguage() {
+ const lang = navigator.language || navigator.userLanguage || '';
+ // Get the base language code (e.g., 'en' from 'en-US', 'zh' from 'zh-CN')
+ const baseLang = lang.split('-')[0].toLowerCase();
+ return baseLang;
+ }
+
+ // Determine the locale to use
+ function determineLocale() {
+ // Check localStorage for saved preference
+ try {
+ const savedLocale = localStorage.getItem('patchReader_locale');
+ if (savedLocale && supportedLocales.includes(savedLocale)) {
+ return savedLocale;
+ }
+ } catch (e) {
+ console.warn('Unable to read saved locale:', e);
+ }
+
+ // Use browser language if supported
+ const browserLang = getBrowserLanguage();
+ if (supportedLocales.includes(browserLang)) {
+ return browserLang;
+ }
+
+ // Default to English
+ return defaultLocale;
+ }
+
+ // Load messages for a locale
+ async function loadMessages(locale) {
+ try {
+ const response = await fetch(`_locales/${locale}/messages.json`);
+ if (!response.ok) {
+ throw new Error(`Failed to load locale: ${locale}`);
+ }
+ return await response.json();
+ } catch (error) {
+ console.warn(`Failed to load locale ${locale}, falling back to ${defaultLocale}:`, error);
+ if (locale !== defaultLocale) {
+ return loadMessages(defaultLocale);
+ }
+ return {};
+ }
+ }
+
+ // Get message by key
+ function getMessage(key) {
+ if (messages[key] && messages[key].message) {
+ return messages[key].message;
+ }
+ return key;
+ }
+
+ // Apply i18n to the page
+ function applyI18n() {
+ // Update elements with data-i18n attribute
+ document.querySelectorAll('[data-i18n]').forEach(element => {
+ const key = element.getAttribute('data-i18n');
+ const message = getMessage(key);
+ if (message !== key) {
+ element.textContent = message;
+ }
+ });
+
+ // Update elements with data-i18n-title attribute
+ document.querySelectorAll('[data-i18n-title]').forEach(element => {
+ const key = element.getAttribute('data-i18n-title');
+ const message = getMessage(key);
+ if (message !== key) {
+ element.title = message;
+ }
+ });
+
+ // Update elements with data-i18n-placeholder attribute
+ document.querySelectorAll('[data-i18n-placeholder]').forEach(element => {
+ const key = element.getAttribute('data-i18n-placeholder');
+ const message = getMessage(key);
+ if (message !== key) {
+ element.placeholder = message;
+ }
+ });
+
+ // Update html lang attribute
+ document.documentElement.lang = currentLocale;
+ }
+
+ // Initialize i18n
+ async function initI18n() {
+ currentLocale = determineLocale();
+ messages = await loadMessages(currentLocale);
+ applyI18n();
+ }
+
+ // Expose to global scope
+ window.i18n = {
+ init: initI18n,
+ getMessage: getMessage,
+ getCurrentLocale: () => currentLocale,
+ getSupportedLocales: () => supportedLocales,
+ setLocale: async (locale) => {
+ if (supportedLocales.includes(locale)) {
+ currentLocale = locale;
+ messages = await loadMessages(locale);
+ try {
+ localStorage.setItem('patchReader_locale', locale);
+ } catch (e) {
+ console.warn('Unable to save locale:', e);
+ }
+ applyI18n();
+ }
+ }
+ };
+
+ // Initialize when DOM is ready
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', initI18n);
+ } else {
+ initI18n();
+ }
+})();
diff --git a/src/common/styles.css b/src/common/styles.css
index 8e1f498..4084d1d 100644
--- a/src/common/styles.css
+++ b/src/common/styles.css
@@ -232,6 +232,10 @@ body {
.d2h-file-header {
background-color: #f6f8fa;
border-bottom: 1px solid #e1e4e8;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-wrap: wrap;
}
.d2h-file-name-wrapper {
@@ -246,6 +250,58 @@ body {
font-size: 12px;
}
+/* Viewed Checkbox Styles */
+.viewed-checkbox-wrapper {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 8px;
+ margin-left: auto;
+ cursor: pointer;
+ border-radius: 4px;
+ transition: background-color 0.2s ease;
+ position: relative;
+ z-index: 10;
+}
+
+.viewed-checkbox-wrapper:hover {
+ background-color: rgba(0, 0, 0, 0.05);
+}
+
+.viewed-checkbox {
+ width: 16px;
+ height: 16px;
+ cursor: pointer;
+ accent-color: #238636;
+}
+
+.viewed-label {
+ font-size: 12px;
+ color: #57606a;
+ user-select: none;
+}
+
+/* File Viewed State - Collapsed */
+.d2h-file-wrapper.file-viewed .d2h-files-diff {
+ display: none;
+}
+
+.d2h-file-wrapper.file-viewed .d2h-file-header {
+ background-color: #dafbe1;
+ border-left: 3px solid #238636;
+}
+
+.d2h-file-wrapper.file-viewed .viewed-label {
+ color: #238636;
+ font-weight: 500;
+}
+
+/* Ensure file header has proper z-index for sticky behavior */
+.d2h-file-header {
+ position: relative;
+ z-index: 5;
+}
+
/* Responsive */
@media (max-width: 768px) {
.header {
From 5cd104f76668a0761e1744b7531aee9686360d48 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 24 Jan 2026 11:49:36 +0000
Subject: [PATCH 3/3] Fix typo: sideBySize -> sideBySide in i18n keys
Co-authored-by: unknowIfGuestInDream <57802425+unknowIfGuestInDream@users.noreply.github.com>
---
src/common/_locales/en/messages.json | 2 +-
src/common/_locales/ja/messages.json | 2 +-
src/common/_locales/zh/messages.json | 2 +-
src/common/diff-viewer.html | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/common/_locales/en/messages.json b/src/common/_locales/en/messages.json
index 2d3b747..ce6313f 100644
--- a/src/common/_locales/en/messages.json
+++ b/src/common/_locales/en/messages.json
@@ -5,7 +5,7 @@
"extDescription": {
"message": "Read and render patch/diff files with diff2html"
},
- "sideBySize": {
+ "sideBySide": {
"message": "Side-by-Side"
},
"unified": {
diff --git a/src/common/_locales/ja/messages.json b/src/common/_locales/ja/messages.json
index f2301b5..ecff61f 100644
--- a/src/common/_locales/ja/messages.json
+++ b/src/common/_locales/ja/messages.json
@@ -5,7 +5,7 @@
"extDescription": {
"message": "diff2htmlでpatch/diffファイルを読み取り、レンダリングします"
},
- "sideBySize": {
+ "sideBySide": {
"message": "サイドバイサイド"
},
"unified": {
diff --git a/src/common/_locales/zh/messages.json b/src/common/_locales/zh/messages.json
index f2aaee8..fb9e27b 100644
--- a/src/common/_locales/zh/messages.json
+++ b/src/common/_locales/zh/messages.json
@@ -5,7 +5,7 @@
"extDescription": {
"message": "使用 diff2html 读取和渲染 patch/diff 文件"
},
- "sideBySize": {
+ "sideBySide": {
"message": "并排对比"
},
"unified": {
diff --git a/src/common/diff-viewer.html b/src/common/diff-viewer.html
index dcaba51..2fb911c 100644
--- a/src/common/diff-viewer.html
+++ b/src/common/diff-viewer.html
@@ -12,7 +12,7 @@