Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6016ee2
Initial plan
Copilot Jul 11, 2025
742d2b8
Convert JavaScript to TypeScript with build setup
Copilot Jul 11, 2025
6fe13bb
Complete TypeScript conversion with cleanup and documentation
Copilot Jul 11, 2025
4b18235
Replace magic numbers with named constants for default window dimensions
Copilot Jul 11, 2025
afe6dc8
Add GitHub Copilot setup workflow for development environment
Copilot Jul 11, 2025
4c71c07
Enhance GitHub Copilot setup workflow with comprehensive validation a…
Copilot Jul 11, 2025
beb114b
Improve Copilot setup workflow with better error handling and validation
Copilot Jul 11, 2025
f8bcbb8
Fix copilot-setup-steps.yml to match official documentation specifica…
Copilot Jul 11, 2025
1e05159
Add Devcontainer support for development environment
Copilot Jul 11, 2025
b197738
Update Node.js to v22 LTS and add Japanese comments to tsconfig.json
Copilot Jul 11, 2025
579f75b
Remove Japanese comments from tsconfig.json for cleaner JSON format
Copilot Jul 11, 2025
38c0e2d
Update src/background.ts
SIkebe Jul 11, 2025
c7636aa
Refactor: Move type definitions to a new types.ts file and update imp…
SIkebe Jul 11, 2025
97d471c
Cleanup: Remove unnecessary comments from copilot-setup-steps.yml for…
SIkebe Jul 11, 2025
39e30a9
Cleanup: Remove npm cache configuration from Copilot setup steps
SIkebe Jul 11, 2025
3361cd5
Apply suggestions from code review
SIkebe Jul 11, 2025
f239cdf
Fix: Update import path for types and correct return type in getDispl…
SIkebe Jul 11, 2025
aea61ba
Refactor: Consolidate type definitions into shared-types.ts and updat…
SIkebe Jul 11, 2025
6192d91
Enhance: Update watch script in package.json to preserve output and a…
SIkebe Jul 11, 2025
eb476f7
Enhance: Add GitHub Actions extension to VSCode customizations in dev…
SIkebe Jul 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "Split Translator Development",
"image": "mcr.microsoft.com/devcontainers/javascript-node:22-bookworm",
"features": {
"ghcr.io/devcontainers/features/github-cli:1": {}
},
"postCreateCommand": "npm install",
"customizations": {
"vscode": {
"extensions": [
"github.vscode-github-actions",
"ms-vscode.vscode-typescript-next"
]
}
},
"remoteUser": "node"
}
33 changes: 33 additions & 0 deletions .github/workflows/copilot-setup-steps.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: "Copilot Setup Steps"

on:
workflow_dispatch:
push:
paths:
- .github/workflows/copilot-setup-steps.yml
pull_request:
paths:
- .github/workflows/copilot-setup-steps.yml

jobs:
# The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot.
copilot-setup-steps:
runs-on: ubuntu-latest

permissions:
contents: read

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "22"

- name: Install JavaScript dependencies
run: npm install

- name: Build TypeScript project
run: npm run build
50 changes: 43 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,21 +110,57 @@ split-translator/
| **Screen Resolution** | Minimum width of 800px is recommended |
| **Browser Version** | Chrome 88+, Edge 88+ (Manifest V3 required) |

## 🛠️ Development & Customization
## 🛠️ Development & Building

### Adjusting Window Overlap
```javascript
const OVERLAP_PIXELS = 8; // Adjustable in background.js
This extension is built with **TypeScript** for enhanced type safety and maintainability.

### Prerequisites
- Node.js (v22 or higher)
- npm

### Building from Source
```bash
# Install dependencies
npm install

# Build TypeScript to JavaScript
npm run build

# Clean build directory
npm run clean

# Watch for changes during development
npm run watch

# Build and package for distribution
npm run package
```

### Project Structure
```
src/
├── background.ts # Service worker (TypeScript)
├── popup.ts # Popup script (TypeScript)
dist/
├── background.js # Compiled service worker
├── popup.js # Compiled popup script
```

### Development & Customization

#### Adjusting Window Overlap
```typescript
const OVERLAP_PIXELS = 8; // Adjustable in src/background.ts
```

### Adding a Language
#### Adding a Language
```html
<!-- Add to the <select> element in popup.html -->
<option value="new_language_code">🏁 New Language Name</option>
```

### Timeout Setting
```javascript
#### Timeout Setting
```typescript
await waitForTabReady(tabId, 3000); // 3 seconds timeout
```

Expand Down
2 changes: 1 addition & 1 deletion build-package.bat
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ if exist "dist\split-translator.zip" del "dist\split-translator.zip"
echo [*] Creating package...

REM Create ZIP file using PowerShell
powershell -Command "& { Add-Type -AssemblyName System.IO.Compression.FileSystem; $zip = [System.IO.Compression.ZipFile]::Open('dist\split-translator.zip', 'Create'); $files = @('manifest.json', 'popup.html', 'popup.js', 'background.js', 'LICENSE', 'PRIVACY_POLICY.md'); foreach ($file in $files) { if (Test-Path $file) { [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip, $file, $file) | Out-Null; Write-Host \"+ $file\" } }; if (Test-Path 'icons') { Get-ChildItem 'icons\*.png' | ForEach-Object { [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip, $_.FullName, 'icons\' + $_.Name) | Out-Null; Write-Host \"+ icons\$($_.Name)\" } }; $zip.Dispose() }"
powershell -Command "& { Add-Type -AssemblyName System.IO.Compression.FileSystem; $zip = [System.IO.Compression.ZipFile]::Open('dist\split-translator.zip', 'Create'); $files = @('manifest.json', 'popup.html', 'dist\popup.js', 'dist\background.js', 'LICENSE', 'PRIVACY_POLICY.md'); foreach ($file in $files) { if (Test-Path $file) { [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip, $file, $file) | Out-Null; Write-Host \"+ $file\" } }; if (Test-Path 'icons') { Get-ChildItem 'icons\*.png' | ForEach-Object { [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip, $_.FullName, 'icons\' + $_.Name) | Out-Null; Write-Host \"+ icons\$($_.Name)\" } }; $zip.Dispose() }"

echo.
echo [OK] Package created successfully: dist\split-translator.zip
Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
}
},
"background": {
"service_worker": "background.js"
"service_worker": "dist/background.js"
},
"icons": {
"16": "icons/icon-16.png",
Expand Down
14 changes: 10 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
"url": "https://github.com/SIkebe/split-translator/issues"
},
"scripts": {
"package": "build-package.bat"
"build": "tsc",
"watch": "tsc --watch --preserveWatchOutput",
"package": "npm run build && build-package.bat",
"clean": "rimraf dist",
"dev": "npm run clean && npm run build && echo 'Development build complete. Load dist/ folder in Chrome Extensions.'"
},
"keywords": [
"browser-extension",
Expand All @@ -31,13 +35,15 @@
},
"license": "MIT",
"devDependencies": {
"@types/chrome": "0.0.329"
"@types/chrome": "0.0.329",
"rimraf": "6.0.1",
"typescript": "5.8.3"
},
"files": [
"manifest.json",
"popup.html",
"popup.js",
"background.js",
"dist/popup.js",
"dist/background.js",
"icons/",
"LICENSE",
"PRIVACY_POLICY.md",
Expand Down
2 changes: 1 addition & 1 deletion popup.html
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,6 @@ <h1>Split Translator</h1>
</div>
</main>

<script src="popup.js"></script>
<script src="dist/popup.js"></script>
</body>
</html>
54 changes: 31 additions & 23 deletions background.js → src/background.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
// Background script (Service Worker)

/// <reference path="shared-types.ts" />

// Constants
const OVERLAP_PIXELS = 8; // Compensate for window frame gaps
const MIN_WINDOW_WIDTH = 400; // Minimum window width in pixels
const MIN_WINDOW_HEIGHT = 300; // Minimum window height in pixels
const DEFAULT_WINDOW_WIDTH = 800; // Default window width when current window width is unavailable
const DEFAULT_WINDOW_HEIGHT = 600; // Default window height when current window height is unavailable

// Common error handler
function handleError(error, context) {
function handleError(error: Error, context: string): { success: false; error: string } {
console.error(`${context}:`, error);
return {
success: false,
Expand All @@ -15,7 +19,7 @@ function handleError(error, context) {
}

// Message listener
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
chrome.runtime.onMessage.addListener((request: SplitAndTranslateMessage, sender, sendResponse) => {
Copy link

Copilot AI Jul 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The message listener doesn't handle rejected promises and is missing a return true; to keep the channel alive for async responses. Add a .catch(error => sendResponse(handleError(error, 'splitAndTranslate'))) after the .then and return true; at the end of the listener.

Copilot uses AI. Check for mistakes.
if (request.action === 'splitAndTranslate') {
handleSplitAndTranslate(request.currentTab, request.targetLanguage)
.then(result => sendResponse(result))
Expand All @@ -25,7 +29,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
});

// Split view handling
async function handleSplitView(currentTab, targetLanguage) {
async function handleSplitView(currentTab: chrome.tabs.Tab, targetLanguage: string): Promise<{ success: true }> {
try {
console.log('Starting split view:', currentTab);

Expand All @@ -44,8 +48,8 @@ async function handleSplitView(currentTab, targetLanguage) {
'file://',
];

if (UNSUPPORTED_PREFIXES.some(prefix => currentTab.url.startsWith(prefix)) ||
currentTab.url.includes('translate.goog')) {
if (UNSUPPORTED_PREFIXES.some(prefix => currentTab.url!.startsWith(prefix)) ||
currentTab.url!.includes('translate.goog')) {
throw new Error('This page type cannot be translated. Please try on a regular website.');
}

Expand All @@ -71,14 +75,14 @@ async function handleSplitView(currentTab, targetLanguage) {
const rightWidth = halfWidth + OVERLAP_PIXELS;

// Calculate positions (use entire display)
const leftPosition = {
const leftPosition: WindowPosition = {
left: displayLeft,
top: displayTop,
width: leftWidth,
height: displayHeight
};

const rightPosition = {
const rightPosition: WindowPosition = {
left: displayLeft + halfWidth - OVERLAP_PIXELS,
top: displayTop,
width: rightWidth,
Expand All @@ -96,15 +100,19 @@ async function handleSplitView(currentTab, targetLanguage) {
state: 'normal'
});

if (!rightWindow || !rightWindow.tabs || !rightWindow.tabs[0]?.id || !rightWindow.id) {
throw new Error('Failed to create right window');
}

// Resize left window
await chrome.windows.update(currentTab.windowId, {
...leftPosition,
state: 'normal'
});

// Save data
const splitViewData = {
originalTabId: currentTab.id,
const splitViewData: SplitViewData = {
originalTabId: currentTab.id!,
duplicatedTabId: rightWindow.tabs[0].id,
targetLanguage: targetLanguage,
originalWindowId: currentTab.windowId,
Expand All @@ -122,12 +130,12 @@ async function handleSplitView(currentTab, targetLanguage) {
}

// Execute split view and translation at once
async function handleSplitAndTranslate(currentTab, targetLanguage) {
async function handleSplitAndTranslate(currentTab: chrome.tabs.Tab, targetLanguage: string): Promise<{ success: true }> {
try {
console.log('Starting split view + translation:', currentTab);

// Prepare translation URL in advance (for parallel processing)
const translateUrl = `https://translate.google.com/translate?sl=auto&tl=${targetLanguage}&u=${encodeURIComponent(currentTab.url)}`;
const translateUrl = `https://translate.google.com/translate?sl=auto&tl=${targetLanguage}&u=${encodeURIComponent(currentTab.url!)}`;

// 1. Execute split view
await handleSplitView(currentTab, targetLanguage);
Expand All @@ -148,7 +156,7 @@ async function handleSplitAndTranslate(currentTab, targetLanguage) {
const currentUrl = rightTab.url;

// Do nothing if URL is already Google Translate page
if (currentUrl.includes('translate.google.com')) {
if (currentUrl && currentUrl.includes('translate.google.com')) {
console.log('Right tab is already a Google Translate page');
return { success: true };
}
Expand Down Expand Up @@ -176,7 +184,7 @@ async function handleSplitAndTranslate(currentTab, targetLanguage) {
}

// Get display information (helper function)
async function getDisplayInfo() {
async function getDisplayInfo(): Promise<chrome.system.display.DisplayUnitInfo[]> {
if (!chrome.system?.display) return [];

return new Promise((resolve) => {
Expand All @@ -192,7 +200,7 @@ async function getDisplayInfo() {
}

// Wait for tab to finish loading (helper function)
async function waitForTabReady(tabId, maxWaitTime = 3000) {
async function waitForTabReady(tabId: number, maxWaitTime: number = 3000): Promise<void> {
const startTime = Date.now();
while (Date.now() - startTime < maxWaitTime) {
try {
Expand All @@ -206,7 +214,7 @@ async function waitForTabReady(tabId, maxWaitTime = 3000) {
}

// Helper function to enforce minimum dimensions
function enforceMinimumDimensions(bounds) {
function enforceMinimumDimensions(bounds: DisplayBounds): DisplayBounds {
return {
...bounds,
width: Math.max(bounds.width, MIN_WINDOW_WIDTH),
Expand All @@ -215,22 +223,22 @@ function enforceMinimumDimensions(bounds) {
}

// Get display bounds (helper function)
function getDisplayBounds(displays, currentWindow) {
let bounds;
function getDisplayBounds(displays: any[], currentWindow: chrome.windows.Window): DisplayBounds {
let bounds: DisplayBounds;

if (!displays || !displays.length) {
console.warn('No display information available, using current window bounds');
bounds = {
left: currentWindow.left, // Preserve current window position
top: currentWindow.top, // Preserve current window position
width: currentWindow.width,
height: currentWindow.height
left: currentWindow.left ?? 0, // Preserve current window position
top: currentWindow.top ?? 0, // Preserve current window position
width: currentWindow.width ?? DEFAULT_WINDOW_WIDTH,
height: currentWindow.height ?? DEFAULT_WINDOW_HEIGHT
};
} else {
// Find the display to which the current window belongs
// Use window center point for accurate detection
const windowCenterX = currentWindow.left + (currentWindow.width / 2);
const windowCenterY = currentWindow.top + (currentWindow.height / 2);
const windowCenterX = (currentWindow.left ?? 0) + ((currentWindow.width ?? DEFAULT_WINDOW_WIDTH) / 2);
const windowCenterY = (currentWindow.top ?? 0) + ((currentWindow.height ?? DEFAULT_WINDOW_HEIGHT) / 2);

const display = displays.find(d =>
windowCenterX >= d.workArea.left &&
Expand Down
Loading