diff --git a/CHANGELOG.md b/CHANGELOG.md
index d0ebbba..b79a870 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,37 +8,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [2.4.0] - 2025-09-30
### 🎉 Major Changes
+
- **Full React Migration**: Migrated popup and options pages to React
- - Modern component-based architecture
- - Improved maintainability and code organization
- - Better state management with React hooks
- - 100% functional parity with vanilla JavaScript version
- - Identical UI/UX experience
+ - Modern component-based architecture
+ - Improved maintainability and code organization
+ - Better state management with React hooks
+ - 100% functional parity with vanilla JavaScript version
+ - Identical UI/UX experience
### ✨ Added
+
- React-based popup interface with custom hooks:
- - `useSettings` for settings management
- - `useTranslation` for i18n support
- - `useLogger` for error tracking
- - `useChromeMessage` for Chrome API integration
+ - `useSettings` for settings management
+ - `useTranslation` for i18n support
+ - `useLogger` for error tracking
+ - `useChromeMessage` for Chrome API integration
- React-based options page with modular sections:
- - `GeneralSection` for general preferences
- - `TranslationSection` for translation settings and batch configuration
- - `ProvidersSection` for provider management
- - `AIContextSection` for AI context configuration
- - `AboutSection` for extension information
+ - `GeneralSection` for general preferences
+ - `TranslationSection` for translation settings and batch configuration
+ - `ProvidersSection` for provider management
+ - `AIContextSection` for AI context configuration
+ - `AboutSection` for extension information
- Reusable React components:
- - `SettingCard`, `ToggleSwitch`, `SettingToggle`
- - `LanguageSelector`, `SliderSetting`, `StatusMessage`
- - `TestResultDisplay`, `SparkleButton`
- - Provider cards for all translation services
+ - `SettingCard`, `ToggleSwitch`, `SettingToggle`
+ - `LanguageSelector`, `SliderSetting`, `StatusMessage`
+ - `TestResultDisplay`, `SparkleButton`
+ - Provider cards for all translation services
- Custom hooks for advanced features:
- - `useDeepLTest` for DeepL API testing
- - `useOpenAITest` for OpenAI API testing and model fetching
- - `useBackgroundReady` for service worker status
+ - `useDeepLTest` for DeepL API testing
+ - `useOpenAITest` for OpenAI API testing and model fetching
+ - `useBackgroundReady` for service worker status
- Vite build system for optimized production bundles
### 🔧 Changed
+
- Build system upgraded from vanilla JavaScript to Vite + React
- Popup and options pages now use React components
- All UI interactions now use React state management
@@ -46,40 +49,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Translation loading uses React effects and state
### 🗑️ Removed
+
- Vanilla JavaScript popup.js and options.js files
- Old HTML templates (replaced by React JSX)
- Manual DOM manipulation code
- jQuery-style event listeners
### 📦 Dependencies
+
- Added `react` ^19.1.1
- Added `react-dom` ^19.1.1
- Added `vite` ^7.1.7
- Added `@vitejs/plugin-react` ^5.0.4
### 🐛 Fixed
+
- Container width consistency in options page across different tabs
- AI Context section structure now matches original layout exactly
- All i18n translation keys corrected to match message definitions
- Proper collapsible Advanced Settings in AI Context section
### 📝 Documentation
+
- Added comprehensive React migration documentation
- Updated README with React-based development information
- Added component architecture documentation
- Updated build and development instructions
### 🔬 Technical Details
+
- Bundle sizes (gzipped):
- - Popup: 13.47 kB (4.58 kB gzipped)
- - Options: 35.24 kB (8.41 kB gzipped)
- - Shared translations: 218.89 kB (66.52 kB gzipped)
+ - Popup: 13.47 kB (4.58 kB gzipped)
+ - Options: 35.24 kB (8.41 kB gzipped)
+ - Shared translations: 218.89 kB (66.52 kB gzipped)
- Build time: ~600ms
- Total React components: 25+
- Custom hooks: 7
- Zero functional differences from vanilla JS version
## [2.3.2] - Previous Version
+
- All previous features and functionality
- Vanilla JavaScript implementation
diff --git a/README_zh.md b/README_zh.md
index 33d10e4..1b311dc 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -84,7 +84,7 @@
```bash
# 生产构建
npm run build
-
+
# 开发模式(自动重新构建)
npm run dev
```
diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index acffd65..88fa1d1 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -192,13 +192,19 @@
"cardOpenAICompatibleDesc": {
"message": "Enter your API key and settings for OpenAI-compatible services like Gemini."
},
- "cardVertexGeminiTitle": { "message": "Vertex AI Gemini (API Key Required)" },
- "cardVertexGeminiDesc": { "message": "Enter your access token and Vertex project settings." },
+ "cardVertexGeminiTitle": {
+ "message": "Vertex AI Gemini (API Key Required)"
+ },
+ "cardVertexGeminiDesc": {
+ "message": "Enter your access token and Vertex project settings."
+ },
"vertexAccessTokenLabel": { "message": "Access Token:" },
"vertexProjectIdLabel": { "message": "Project ID:" },
"vertexLocationLabel": { "message": "Location:" },
"vertexModelLabel": { "message": "Model:" },
- "vertexMissingConfig": { "message": "Please enter access token and project ID." },
+ "vertexMissingConfig": {
+ "message": "Please enter access token and project ID."
+ },
"vertexConnectionFailed": { "message": "Connection failed: %s" },
"vertexServiceAccountLabel": { "message": "Service Account JSON:" },
"vertexImportButton": { "message": "Import JSON File" },
@@ -207,19 +213,33 @@
"vertexImporting": { "message": "Importing..." },
"vertexRefreshingToken": { "message": "Refreshing access token..." },
"vertexGeneratingToken": { "message": "Generating access token..." },
- "vertexImportSuccess": { "message": "Service account imported and token generated." },
+ "vertexImportSuccess": {
+ "message": "Service account imported and token generated."
+ },
"vertexImportFailed": { "message": "Import failed: %s" },
- "vertexTokenRefreshed": { "message": "Access token refreshed successfully." },
+ "vertexTokenRefreshed": {
+ "message": "Access token refreshed successfully."
+ },
"vertexRefreshFailed": { "message": "Token refresh failed: %s" },
- "vertexTokenExpired": { "message": "⚠️ Access token expired. Click refresh to renew." },
- "vertexTokenExpiringSoon": { "message": "⚠️ Token expires in %s minutes. Consider refreshing." },
- "vertexConfigured": { "message": "⚠️ Vertex AI configured. Please test connection." },
- "vertexNotConfigured": { "message": "Please import service account JSON or enter credentials." },
+ "vertexTokenExpired": {
+ "message": "⚠️ Access token expired. Click refresh to renew."
+ },
+ "vertexTokenExpiringSoon": {
+ "message": "⚠️ Token expires in %s minutes. Consider refreshing."
+ },
+ "vertexConfigured": {
+ "message": "⚠️ Vertex AI configured. Please test connection."
+ },
+ "vertexNotConfigured": {
+ "message": "Please import service account JSON or enter credentials."
+ },
"featureVertexServiceAccount": { "message": "Service account JSON import" },
"featureVertexAutoToken": { "message": "Automatic token generation" },
"featureVertexGemini": { "message": "Google Gemini models via Vertex AI" },
"providerNote": { "message": "Note:" },
- "vertexNote": { "message": "Access tokens expire after 1 hour. Your service account is securely stored for easy token refresh - just click the Refresh Token button when needed." },
+ "vertexNote": {
+ "message": "Access tokens expire after 1 hour. Your service account is securely stored for easy token refresh - just click the Refresh Token button when needed."
+ },
"baseUrlLabel": { "message": "Base URL:" },
"modelLabel": { "message": "Model:" },
"featureCustomizable": { "message": "Customizable endpoint and model" },
diff --git a/_locales/es/messages.json b/_locales/es/messages.json
index d04809d..af2f976 100644
--- a/_locales/es/messages.json
+++ b/_locales/es/messages.json
@@ -207,33 +207,59 @@
"cardOpenAICompatibleDesc": {
"message": "Ingresa tu clave API y configuraciones para servicios compatibles con OpenAI como Gemini."
},
- "cardVertexGeminiTitle": { "message": "Vertex AI Gemini (Requiere Clave API)" },
- "cardVertexGeminiDesc": { "message": "Ingresa tu token de acceso y configuraciones del proyecto Vertex, o importa un archivo JSON de cuenta de servicio." },
+ "cardVertexGeminiTitle": {
+ "message": "Vertex AI Gemini (Requiere Clave API)"
+ },
+ "cardVertexGeminiDesc": {
+ "message": "Ingresa tu token de acceso y configuraciones del proyecto Vertex, o importa un archivo JSON de cuenta de servicio."
+ },
"vertexAccessTokenLabel": { "message": "Token de Acceso:" },
"vertexProjectIdLabel": { "message": "ID del Proyecto:" },
"vertexLocationLabel": { "message": "Ubicación:" },
"vertexModelLabel": { "message": "Modelo:" },
- "vertexMissingConfig": { "message": "Por favor ingresa el token de acceso y el ID del proyecto." },
+ "vertexMissingConfig": {
+ "message": "Por favor ingresa el token de acceso y el ID del proyecto."
+ },
"vertexConnectionFailed": { "message": "Conexión fallida: %s" },
"vertexServiceAccountLabel": { "message": "JSON de Cuenta de Servicio:" },
"vertexImportButton": { "message": "Importar Archivo JSON" },
"vertexRefreshButton": { "message": "🔄 Actualizar Token" },
- "vertexImportHint": { "message": "Rellena automáticamente las credenciales a continuación" },
+ "vertexImportHint": {
+ "message": "Rellena automáticamente las credenciales a continuación"
+ },
"vertexImporting": { "message": "Importando..." },
"vertexRefreshingToken": { "message": "Actualizando token de acceso..." },
"vertexGeneratingToken": { "message": "Generando token de acceso..." },
- "vertexImportSuccess": { "message": "Cuenta de servicio importada y token generado." },
+ "vertexImportSuccess": {
+ "message": "Cuenta de servicio importada y token generado."
+ },
"vertexImportFailed": { "message": "Importación fallida: %s" },
- "vertexTokenRefreshed": { "message": "Token de acceso actualizado exitosamente." },
+ "vertexTokenRefreshed": {
+ "message": "Token de acceso actualizado exitosamente."
+ },
"vertexRefreshFailed": { "message": "Actualización de token fallida: %s" },
- "vertexTokenExpired": { "message": "⚠️ Token de acceso expirado. Haz clic en actualizar para renovar." },
- "vertexTokenExpiringSoon": { "message": "⚠️ El token expira en %s minutos. Considera actualizarlo." },
- "vertexConfigured": { "message": "⚠️ Vertex AI configurado. Por favor prueba la conexión." },
- "vertexNotConfigured": { "message": "Por favor importa el JSON de cuenta de servicio o ingresa las credenciales." },
- "featureVertexServiceAccount": { "message": "Importación de JSON de cuenta de servicio" },
+ "vertexTokenExpired": {
+ "message": "⚠️ Token de acceso expirado. Haz clic en actualizar para renovar."
+ },
+ "vertexTokenExpiringSoon": {
+ "message": "⚠️ El token expira en %s minutos. Considera actualizarlo."
+ },
+ "vertexConfigured": {
+ "message": "⚠️ Vertex AI configurado. Por favor prueba la conexión."
+ },
+ "vertexNotConfigured": {
+ "message": "Por favor importa el JSON de cuenta de servicio o ingresa las credenciales."
+ },
+ "featureVertexServiceAccount": {
+ "message": "Importación de JSON de cuenta de servicio"
+ },
"featureVertexAutoToken": { "message": "Generación automática de tokens" },
- "featureVertexGemini": { "message": "Modelos Google Gemini a través de Vertex AI" },
- "vertexNote": { "message": "Los tokens de acceso expiran después de 1 hora. Tu cuenta de servicio está almacenada de forma segura para facilitar la actualización del token - solo haz clic en el botón Actualizar Token cuando sea necesario." },
+ "featureVertexGemini": {
+ "message": "Modelos Google Gemini a través de Vertex AI"
+ },
+ "vertexNote": {
+ "message": "Los tokens de acceso expiran después de 1 hora. Tu cuenta de servicio está almacenada de forma segura para facilitar la actualización del token - solo haz clic en el botón Actualizar Token cuando sea necesario."
+ },
"baseUrlLabel": { "message": "URL Base:" },
"modelLabel": { "message": "Modelo:" },
"featureCustomizable": { "message": "Endpoint y modelo personalizables" },
diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json
index 483428d..d3c64ed 100644
--- a/_locales/ja/messages.json
+++ b/_locales/ja/messages.json
@@ -186,18 +186,24 @@
"providerDeepLName": { "message": "DeepL(APIキー必須)" },
"providerDeepLFreeName": { "message": "DeepL翻訳(無料)" },
"providerOpenAICompatibleName": { "message": "OpenAI互換(APIキー必須)" },
- "providerVertexGeminiName": { "message": "Vertex AI Gemini(APIキー必須)" },
+ "providerVertexGeminiName": {
+ "message": "Vertex AI Gemini(APIキー必須)"
+ },
"cardOpenAICompatibleTitle": { "message": "OpenAI互換(APIキー必須)" },
"cardOpenAICompatibleDesc": {
"message": "GeminiなどのOpenAI互換サービス用のAPIキーと設定を入力してください。"
},
"cardVertexGeminiTitle": { "message": "Vertex AI Gemini(APIキー必須)" },
- "cardVertexGeminiDesc": { "message": "アクセストークンとVertex プロジェクト設定を入力するか、サービスアカウントJSONファイルをインポートしてください。" },
+ "cardVertexGeminiDesc": {
+ "message": "アクセストークンとVertex プロジェクト設定を入力するか、サービスアカウントJSONファイルをインポートしてください。"
+ },
"vertexAccessTokenLabel": { "message": "アクセストークン:" },
"vertexProjectIdLabel": { "message": "プロジェクトID:" },
"vertexLocationLabel": { "message": "ロケーション:" },
"vertexModelLabel": { "message": "モデル:" },
- "vertexMissingConfig": { "message": "アクセストークンとプロジェクトIDを入力してください。" },
+ "vertexMissingConfig": {
+ "message": "アクセストークンとプロジェクトIDを入力してください。"
+ },
"vertexConnectionFailed": { "message": "接続に失敗しました:%s" },
"vertexServiceAccountLabel": { "message": "サービスアカウントJSON:" },
"vertexImportButton": { "message": "JSONファイルをインポート" },
@@ -206,18 +212,34 @@
"vertexImporting": { "message": "インポート中..." },
"vertexRefreshingToken": { "message": "アクセストークンを更新中..." },
"vertexGeneratingToken": { "message": "アクセストークンを生成中..." },
- "vertexImportSuccess": { "message": "サービスアカウントがインポートされ、トークンが生成されました。" },
+ "vertexImportSuccess": {
+ "message": "サービスアカウントがインポートされ、トークンが生成されました。"
+ },
"vertexImportFailed": { "message": "インポートに失敗しました:%s" },
- "vertexTokenRefreshed": { "message": "アクセストークンが正常に更新されました。" },
+ "vertexTokenRefreshed": {
+ "message": "アクセストークンが正常に更新されました。"
+ },
"vertexRefreshFailed": { "message": "トークンの更新に失敗しました:%s" },
- "vertexTokenExpired": { "message": "⚠️ アクセストークンが期限切れです。更新をクリックして更新してください。" },
- "vertexTokenExpiringSoon": { "message": "⚠️ トークンは%s分で期限切れになります。更新を検討してください。" },
- "vertexConfigured": { "message": "⚠️ Vertex AIが設定されています。接続をテストしてください。" },
- "vertexNotConfigured": { "message": "サービスアカウントJSONをインポートするか、認証情報を入力してください。" },
- "featureVertexServiceAccount": { "message": "サービスアカウントJSONのインポート" },
+ "vertexTokenExpired": {
+ "message": "⚠️ アクセストークンが期限切れです。更新をクリックして更新してください。"
+ },
+ "vertexTokenExpiringSoon": {
+ "message": "⚠️ トークンは%s分で期限切れになります。更新を検討してください。"
+ },
+ "vertexConfigured": {
+ "message": "⚠️ Vertex AIが設定されています。接続をテストしてください。"
+ },
+ "vertexNotConfigured": {
+ "message": "サービスアカウントJSONをインポートするか、認証情報を入力してください。"
+ },
+ "featureVertexServiceAccount": {
+ "message": "サービスアカウントJSONのインポート"
+ },
"featureVertexAutoToken": { "message": "自動トークン生成" },
"featureVertexGemini": { "message": "Vertex AI経由のGoogle Geminiモデル" },
- "vertexNote": { "message": "アクセストークンは1時間後に期限切れになります。サービスアカウントは安全に保存されており、簡単にトークンを更新できます - 必要に応じてトークン更新ボタンをクリックしてください。" },
+ "vertexNote": {
+ "message": "アクセストークンは1時間後に期限切れになります。サービスアカウントは安全に保存されており、簡単にトークンを更新できます - 必要に応じてトークン更新ボタンをクリックしてください。"
+ },
"baseUrlLabel": { "message": "ベースURL:" },
"modelLabel": { "message": "モデル:" },
"featureCustomizable": {
diff --git a/_locales/ko/messages.json b/_locales/ko/messages.json
index ed66994..8102b13 100644
--- a/_locales/ko/messages.json
+++ b/_locales/ko/messages.json
@@ -190,12 +190,16 @@
"message": "Gemini와 같은 OpenAI 호환 서비스를 위한 API 키와 설정을 입력하세요."
},
"cardVertexGeminiTitle": { "message": "Vertex AI Gemini (API 키 필요)" },
- "cardVertexGeminiDesc": { "message": "액세스 토큰과 Vertex 프로젝트 설정을 입력하거나 서비스 계정 JSON 파일을 가져오세요." },
+ "cardVertexGeminiDesc": {
+ "message": "액세스 토큰과 Vertex 프로젝트 설정을 입력하거나 서비스 계정 JSON 파일을 가져오세요."
+ },
"vertexAccessTokenLabel": { "message": "액세스 토큰:" },
"vertexProjectIdLabel": { "message": "프로젝트 ID:" },
"vertexLocationLabel": { "message": "위치:" },
"vertexModelLabel": { "message": "모델:" },
- "vertexMissingConfig": { "message": "액세스 토큰과 프로젝트 ID를 입력하세요." },
+ "vertexMissingConfig": {
+ "message": "액세스 토큰과 프로젝트 ID를 입력하세요."
+ },
"vertexConnectionFailed": { "message": "연결 실패: %s" },
"vertexServiceAccountLabel": { "message": "서비스 계정 JSON:" },
"vertexImportButton": { "message": "JSON 파일 가져오기" },
@@ -204,18 +208,32 @@
"vertexImporting": { "message": "가져오는 중..." },
"vertexRefreshingToken": { "message": "액세스 토큰 새로고침 중..." },
"vertexGeneratingToken": { "message": "액세스 토큰 생성 중..." },
- "vertexImportSuccess": { "message": "서비스 계정을 가져오고 토큰을 생성했습니다." },
+ "vertexImportSuccess": {
+ "message": "서비스 계정을 가져오고 토큰을 생성했습니다."
+ },
"vertexImportFailed": { "message": "가져오기 실패: %s" },
- "vertexTokenRefreshed": { "message": "액세스 토큰이 성공적으로 새로고침되었습니다." },
+ "vertexTokenRefreshed": {
+ "message": "액세스 토큰이 성공적으로 새로고침되었습니다."
+ },
"vertexRefreshFailed": { "message": "토큰 새로고침 실패: %s" },
- "vertexTokenExpired": { "message": "⚠️ 액세스 토큰이 만료되었습니다. 새로고침을 클릭하여 갱신하세요." },
- "vertexTokenExpiringSoon": { "message": "⚠️ 토큰이 %s분 후에 만료됩니다. 새로고침을 고려하세요." },
- "vertexConfigured": { "message": "⚠️ Vertex AI가 구성되었습니다. 연결을 테스트하세요." },
- "vertexNotConfigured": { "message": "서비스 계정 JSON을 가져오거나 자격 증명을 입력하세요." },
+ "vertexTokenExpired": {
+ "message": "⚠️ 액세스 토큰이 만료되었습니다. 새로고침을 클릭하여 갱신하세요."
+ },
+ "vertexTokenExpiringSoon": {
+ "message": "⚠️ 토큰이 %s분 후에 만료됩니다. 새로고침을 고려하세요."
+ },
+ "vertexConfigured": {
+ "message": "⚠️ Vertex AI가 구성되었습니다. 연결을 테스트하세요."
+ },
+ "vertexNotConfigured": {
+ "message": "서비스 계정 JSON을 가져오거나 자격 증명을 입력하세요."
+ },
"featureVertexServiceAccount": { "message": "서비스 계정 JSON 가져오기" },
"featureVertexAutoToken": { "message": "자동 토큰 생성" },
"featureVertexGemini": { "message": "Vertex AI를 통한 Google Gemini 모델" },
- "vertexNote": { "message": "액세스 토큰은 1시간 후에 만료됩니다. 서비스 계정은 안전하게 저장되어 쉽게 토큰을 새로고침할 수 있습니다 - 필요할 때 토큰 새로고침 버튼을 클릭하세요." },
+ "vertexNote": {
+ "message": "액세스 토큰은 1시간 후에 만료됩니다. 서비스 계정은 안전하게 저장되어 쉽게 토큰을 새로고침할 수 있습니다 - 필요할 때 토큰 새로고침 버튼을 클릭하세요."
+ },
"baseUrlLabel": { "message": "기본 URL:" },
"modelLabel": { "message": "모델:" },
"featureCustomizable": {
diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json
index 61c8a50..440b560 100644
--- a/_locales/zh_CN/messages.json
+++ b/_locales/zh_CN/messages.json
@@ -163,7 +163,9 @@
"message": "输入您的 API 密钥和设置,用于 Gemini 等 OpenAI 兼容服务。"
},
"cardVertexGeminiTitle": { "message": "Vertex AI Gemini(需要 API 密钥)" },
- "cardVertexGeminiDesc": { "message": "输入您的访问令牌和 Vertex 项目设置,或导入服务账号 JSON 文件。" },
+ "cardVertexGeminiDesc": {
+ "message": "输入您的访问令牌和 Vertex 项目设置,或导入服务账号 JSON 文件。"
+ },
"vertexAccessTokenLabel": { "message": "访问令牌:" },
"vertexProjectIdLabel": { "message": "项目 ID:" },
"vertexLocationLabel": { "message": "位置:" },
@@ -182,14 +184,20 @@
"vertexTokenRefreshed": { "message": "访问令牌刷新成功。" },
"vertexRefreshFailed": { "message": "令牌刷新失败:%s" },
"vertexTokenExpired": { "message": "⚠️ 访问令牌已过期。点击刷新以续期。" },
- "vertexTokenExpiringSoon": { "message": "⚠️ 令牌将在 %s 分钟后过期。建议刷新。" },
+ "vertexTokenExpiringSoon": {
+ "message": "⚠️ 令牌将在 %s 分钟后过期。建议刷新。"
+ },
"vertexConfigured": { "message": "⚠️ Vertex AI 已配置。请测试连接。" },
"vertexNotConfigured": { "message": "请导入服务账号 JSON 或输入凭据。" },
"featureVertexServiceAccount": { "message": "服务账号 JSON 导入" },
"featureVertexAutoToken": { "message": "自动生成令牌" },
- "featureVertexGemini": { "message": "通过 Vertex AI 使用 Google Gemini 模型" },
+ "featureVertexGemini": {
+ "message": "通过 Vertex AI 使用 Google Gemini 模型"
+ },
"providerNote": { "message": "注意:" },
- "vertexNote": { "message": "访问令牌在 1 小时后过期。您的服务账号已安全存储,需要时只需点击刷新令牌按钮即可。" },
+ "vertexNote": {
+ "message": "访问令牌在 1 小时后过期。您的服务账号已安全存储,需要时只需点击刷新令牌按钮即可。"
+ },
"baseUrlLabel": { "message": "基础 URL:" },
"modelLabel": { "message": "模型:" },
"featureCustomizable": { "message": "可自定义端点和模型" },
diff --git a/_locales/zh_TW/messages.json b/_locales/zh_TW/messages.json
index 642b2ee..4fe520b 100644
--- a/_locales/zh_TW/messages.json
+++ b/_locales/zh_TW/messages.json
@@ -166,7 +166,9 @@
"message": "輸入您的 API 金鑰和設定,用於 Gemini 等 OpenAI 相容服務。"
},
"cardVertexGeminiTitle": { "message": "Vertex AI Gemini(需要 API 金鑰)" },
- "cardVertexGeminiDesc": { "message": "輸入您的存取權杖和 Vertex 專案設定,或匯入服務帳戶 JSON 檔案。" },
+ "cardVertexGeminiDesc": {
+ "message": "輸入您的存取權杖和 Vertex 專案設定,或匯入服務帳戶 JSON 檔案。"
+ },
"vertexAccessTokenLabel": { "message": "存取權杖:" },
"vertexProjectIdLabel": { "message": "專案 ID:" },
"vertexLocationLabel": { "message": "位置:" },
@@ -184,14 +186,22 @@
"vertexImportFailed": { "message": "匯入失敗:%s" },
"vertexTokenRefreshed": { "message": "存取權杖已成功重新整理。" },
"vertexRefreshFailed": { "message": "權杖重新整理失敗:%s" },
- "vertexTokenExpired": { "message": "⚠️ 存取權杖已過期。點擊重新整理以更新。" },
- "vertexTokenExpiringSoon": { "message": "⚠️ 權杖將在 %s 分鐘後過期。建議重新整理。" },
+ "vertexTokenExpired": {
+ "message": "⚠️ 存取權杖已過期。點擊重新整理以更新。"
+ },
+ "vertexTokenExpiringSoon": {
+ "message": "⚠️ 權杖將在 %s 分鐘後過期。建議重新整理。"
+ },
"vertexConfigured": { "message": "⚠️ Vertex AI 已設定。請測試連線。" },
"vertexNotConfigured": { "message": "請匯入服務帳戶 JSON 或輸入憑證。" },
"featureVertexServiceAccount": { "message": "服務帳戶 JSON 匯入" },
"featureVertexAutoToken": { "message": "自動產生權杖" },
- "featureVertexGemini": { "message": "透過 Vertex AI 使用 Google Gemini 模型" },
- "vertexNote": { "message": "存取權杖在 1 小時後過期。您的服務帳戶已安全儲存,需要時只需點擊重新整理權杖按鈕即可。" },
+ "featureVertexGemini": {
+ "message": "透過 Vertex AI 使用 Google Gemini 模型"
+ },
+ "vertexNote": {
+ "message": "存取權杖在 1 小時後過期。您的服務帳戶已安全儲存,需要時只需點擊重新整理權杖按鈕即可。"
+ },
"baseUrlLabel": { "message": "基礎 URL:" },
"modelLabel": { "message": "模型:" },
"featureCustomizable": { "message": "可自訂端點和模型" },
diff --git a/background/services/translationService.js b/background/services/translationService.js
index 86e6780..548c2da 100644
--- a/background/services/translationService.js
+++ b/background/services/translationService.js
@@ -32,7 +32,10 @@ import {
ProviderNames,
ProviderBatchConfigs,
} from '../../content_scripts/shared/constants/providers.js';
-import { translate as vertexGeminiTranslate, translateBatch as vertexGeminiTranslateBatch } from '../../translation_providers/geminiVertexTranslate.js';
+import {
+ translate as vertexGeminiTranslate,
+ translateBatch as vertexGeminiTranslateBatch,
+} from '../../translation_providers/geminiVertexTranslate.js';
import TTLCache from '../../utils/cache/TTLCache.js';
/**
@@ -136,10 +139,13 @@ class TranslationService {
},
category: 'api_key',
batchOptimizations: {
- maxBatchSize: ProviderBatchConfigs[Providers.VERTEX_GEMINI].maxBatchSize,
+ maxBatchSize:
+ ProviderBatchConfigs[Providers.VERTEX_GEMINI]
+ .maxBatchSize,
contextPreservation: true,
exponentialBackoff: true,
- delimiter: ProviderBatchConfigs[Providers.VERTEX_GEMINI].delimiter,
+ delimiter:
+ ProviderBatchConfigs[Providers.VERTEX_GEMINI].delimiter,
},
},
};
diff --git a/config/configSchema.js b/config/configSchema.js
index a27f663..2160cbb 100644
--- a/config/configSchema.js
+++ b/config/configSchema.js
@@ -86,8 +86,16 @@ export const configSchema = {
// Vertex AI Gemini Translation Settings
vertexAccessToken: { defaultValue: '', type: String, scope: 'sync' },
vertexProjectId: { defaultValue: '', type: String, scope: 'sync' },
- vertexLocation: { defaultValue: 'us-central1', type: String, scope: 'sync' },
- vertexModel: { defaultValue: 'gemini-2.5-flash', type: String, scope: 'sync' },
+ vertexLocation: {
+ defaultValue: 'us-central1',
+ type: String,
+ scope: 'sync',
+ },
+ vertexModel: {
+ defaultValue: 'gemini-2.5-flash',
+ type: String,
+ scope: 'sync',
+ },
// --- Subtitle Settings (from popup.js & background.js defaults) ---
subtitlesEnabled: { defaultValue: true, type: Boolean, scope: 'sync' },
diff --git a/content_scripts/core/BaseContentScript.js b/content_scripts/core/BaseContentScript.js
index 74c8915..e864f46 100644
--- a/content_scripts/core/BaseContentScript.js
+++ b/content_scripts/core/BaseContentScript.js
@@ -1749,7 +1749,10 @@ export class BaseContentScript {
async _initializeBasedOnPageType() {
// Check if platform was cleaned up during initialization
if (!this.activePlatform) {
- this.logWithFallback('warn', 'Platform cleaned up during initialization, aborting');
+ this.logWithFallback(
+ 'warn',
+ 'Platform cleaned up during initialization, aborting'
+ );
return false;
}
@@ -1769,13 +1772,16 @@ export class BaseContentScript {
this.logWithFallback('info', 'Initializing platform on player page');
await this._initializePlatformWithTimeout();
-
+
// Check if platform was cleaned up during async initialization
if (!this.activePlatform) {
- this.logWithFallback('warn', 'Platform cleaned up during player page initialization, aborting');
+ this.logWithFallback(
+ 'warn',
+ 'Platform cleaned up during player page initialization, aborting'
+ );
return false;
}
-
+
this.activePlatform.handleNativeSubtitles();
this.platformReady = true;
diff --git a/docs/en/installation.md b/docs/en/installation.md
index 1564775..d4d3da9 100644
--- a/docs/en/installation.md
+++ b/docs/en/installation.md
@@ -66,17 +66,17 @@
```
3. Build the extension
-
+
The extension uses React and requires building before use:
-
+
```bash
npm run build
```
-
+
This will create a `dist/` folder with the compiled extension.
-
+
For development with auto-rebuild:
-
+
```bash
npm run dev
```
@@ -110,9 +110,9 @@
## Troubleshooting
- Extension not visible: ensure it's enabled at `chrome://extensions` and optionally pinned in the toolbar
-- "Could not load manifest":
- - For GitHub releases: make sure you extracted the ZIP and selected the extracted folder
- - For development: make sure you selected the `dist/` folder (not the project root!) and ran `npm run build` first
+- "Could not load manifest":
+ - For GitHub releases: make sure you extracted the ZIP and selected the extracted folder
+ - For development: make sure you selected the `dist/` folder (not the project root!) and ran `npm run build` first
- Build errors: ensure you have Node.js 18+ installed and run `npm install` before `npm run build`
- No subtitles: verify the platform provides subtitles and they are enabled in the player
- AI Context not working: set your API key and model in Advanced Settings; check rate limits and network connectivity
diff --git a/docs/zh/installation.md b/docs/zh/installation.md
index d923893..ac93585 100644
--- a/docs/zh/installation.md
+++ b/docs/zh/installation.md
@@ -66,17 +66,17 @@
```
3. 构建扩展
-
+
扩展使用 React 开发,使用前需要构建:
-
+
```bash
npm run build
```
-
+
这将创建 `dist/` 文件夹,其中包含编译后的扩展。
-
+
开发模式下自动重新构建:
-
+
```bash
npm run dev
```
@@ -111,8 +111,8 @@
- 扩展不可见:在 `chrome://extensions` 确认已启用,并可选择固定到工具栏
- "无法加载 manifest":
- - GitHub 发布版:确保已解压 ZIP 并选择解压后的文件夹
- - 开发版:确保选择的是 `dist/` 文件夹(不是项目根目录!)并且已运行 `npm run build`
+ - GitHub 发布版:确保已解压 ZIP 并选择解压后的文件夹
+ - 开发版:确保选择的是 `dist/` 文件夹(不是项目根目录!)并且已运行 `npm run build`
- 构建错误:确保已安装 Node.js 18+,并在 `npm run build` 之前运行 `npm install`
- 无字幕可用:确认平台本身提供字幕并在播放器中已开启
- AI 上下文无响应:在高级设置中配置 API 密钥与模型;检查速率限制与网络
diff --git a/options/OptionsApp.jsx b/options/OptionsApp.jsx
index 56b24f6..f117660 100644
--- a/options/OptionsApp.jsx
+++ b/options/OptionsApp.jsx
@@ -24,7 +24,7 @@ export function OptionsApp() {
const handleSettingChange = async (key, value) => {
await updateSetting(key, value);
-
+
// If language changes, reload translations
if (key === 'uiLanguage') {
setCurrentLanguage(value);
diff --git a/options/components/AppleStyleFileButton.jsx b/options/components/AppleStyleFileButton.jsx
index 2346b59..b82ab70 100644
--- a/options/components/AppleStyleFileButton.jsx
+++ b/options/components/AppleStyleFileButton.jsx
@@ -4,12 +4,12 @@ import React from 'react';
* Apple-style file upload button component
* Mimics the design of macOS file selection buttons
*/
-export function AppleStyleFileButton({
- onClick,
- disabled,
- children,
+export function AppleStyleFileButton({
+ onClick,
+ disabled,
+ children,
className = '',
- loading = false
+ loading = false,
}) {
return (
) : (
-
-
-
-
@@ -57,4 +57,3 @@ export function AppleStyleFileButton({
);
}
-
diff --git a/options/components/SparkleButton.jsx b/options/components/SparkleButton.jsx
index d8e1daf..44045d7 100644
--- a/options/components/SparkleButton.jsx
+++ b/options/components/SparkleButton.jsx
@@ -8,7 +8,12 @@ export function SparkleButton({ onClick, disabled, children, className = '' }) {
onClick={onClick}
disabled={disabled}
>
-
+
diff --git a/options/components/TestResultDisplay.jsx b/options/components/TestResultDisplay.jsx
index 1ab06a2..1186a91 100644
--- a/options/components/TestResultDisplay.jsx
+++ b/options/components/TestResultDisplay.jsx
@@ -5,9 +5,5 @@ export function TestResultDisplay({ result }) {
return null;
}
- return (
-
- {result.message}
-
- );
+ return {result.message}
;
}
diff --git a/options/components/providers/DeepLFreeProviderCard.jsx b/options/components/providers/DeepLFreeProviderCard.jsx
index da8d4dd..99e3601 100644
--- a/options/components/providers/DeepLFreeProviderCard.jsx
+++ b/options/components/providers/DeepLFreeProviderCard.jsx
@@ -22,17 +22,47 @@ export function DeepLFreeProviderCard({ t }) {
{t('featureFree', 'Free to use')}
{t('featureNoApiKey', 'No API key required')}
- {t('featureHighestQuality', 'Highest quality translation')}
- {t('featureWideLanguageSupport', 'Wide language support')}
- {t('featureMultipleBackups', 'Multiple backup methods')}
+
+ {t(
+ 'featureHighestQuality',
+ 'Highest quality translation'
+ )}
+
+
+ {t(
+ 'featureWideLanguageSupport',
+ 'Wide language support'
+ )}
+
+
+ {t(
+ 'featureMultipleBackups',
+ 'Multiple backup methods'
+ )}
+
{t('providerNotes', 'Notes:')}
- {t('noteSlowForSecurity', 'Slightly slower due to security measures')}
- {t('noteAutoFallback', 'Automatic fallback to alternative services')}
- {t('noteRecommendedDefault', 'Recommended as default provider')}
+
+ {t(
+ 'noteSlowForSecurity',
+ 'Slightly slower due to security measures'
+ )}
+
+
+ {t(
+ 'noteAutoFallback',
+ 'Automatic fallback to alternative services'
+ )}
+
+
+ {t(
+ 'noteRecommendedDefault',
+ 'Recommended as default provider'
+ )}
+
diff --git a/options/components/providers/DeepLProviderCard.jsx b/options/components/providers/DeepLProviderCard.jsx
index daa9e50..6553dec 100644
--- a/options/components/providers/DeepLProviderCard.jsx
+++ b/options/components/providers/DeepLProviderCard.jsx
@@ -4,8 +4,15 @@ import { SparkleButton } from '../SparkleButton.jsx';
import { TestResultDisplay } from '../TestResultDisplay.jsx';
import { useDeepLTest } from '../../hooks/index.js';
-export function DeepLProviderCard({ t, apiKey, apiPlan, onApiKeyChange, onApiPlanChange }) {
- const { testResult, testing, testConnection, initializeStatus } = useDeepLTest(t);
+export function DeepLProviderCard({
+ t,
+ apiKey,
+ apiPlan,
+ onApiKeyChange,
+ onApiPlanChange,
+}) {
+ const { testResult, testing, testConnection, initializeStatus } =
+ useDeepLTest(t);
// Initialize status when component mounts or API key changes
useEffect(() => {
@@ -61,10 +68,9 @@ export function DeepLProviderCard({ t, apiKey, apiPlan, onApiKeyChange, onApiPla
onClick={handleTest}
disabled={testing || !apiKey}
>
- {testing
+ {testing
? t('testingButton', 'Testing...')
- : t('testDeepLButton', 'Test DeepL Connection')
- }
+ : t('testDeepLButton', 'Test DeepL Connection')}
@@ -72,9 +78,21 @@ export function DeepLProviderCard({ t, apiKey, apiPlan, onApiKeyChange, onApiPla
{t('providerFeatures', 'Features:')}
- {t('featureHighestQuality', 'Highest quality translation')}
- {t('featureApiKeyRequired', 'API key required')}
- {t('featureLimitedLanguages', 'Limited language support')}
+
+ {t(
+ 'featureHighestQuality',
+ 'Highest quality translation'
+ )}
+
+
+ {t('featureApiKeyRequired', 'API key required')}
+
+
+ {t(
+ 'featureLimitedLanguages',
+ 'Limited language support'
+ )}
+
{t('featureUsageLimits', 'Usage limits apply')}
diff --git a/options/components/providers/GoogleProviderCard.jsx b/options/components/providers/GoogleProviderCard.jsx
index 220190f..9d10afc 100644
--- a/options/components/providers/GoogleProviderCard.jsx
+++ b/options/components/providers/GoogleProviderCard.jsx
@@ -22,8 +22,15 @@ export function GoogleProviderCard({ t }) {
{t('featureFree', 'Free to use')}
{t('featureNoApiKey', 'No API key required')}
- {t('featureWideLanguageSupport', 'Wide language support')}
- {t('featureFastTranslation', 'Fast translation')}
+
+ {t(
+ 'featureWideLanguageSupport',
+ 'Wide language support'
+ )}
+
+
+ {t('featureFastTranslation', 'Fast translation')}
+
diff --git a/options/components/providers/MicrosoftProviderCard.jsx b/options/components/providers/MicrosoftProviderCard.jsx
index 67f5632..6a632e8 100644
--- a/options/components/providers/MicrosoftProviderCard.jsx
+++ b/options/components/providers/MicrosoftProviderCard.jsx
@@ -22,8 +22,15 @@ export function MicrosoftProviderCard({ t }) {
{t('featureFree', 'Free to use')}
{t('featureNoApiKey', 'No API key required')}
- {t('featureHighQuality', 'High quality translation')}
- {t('featureGoodPerformance', 'Good performance')}
+
+ {t(
+ 'featureHighQuality',
+ 'High quality translation'
+ )}
+
+
+ {t('featureGoodPerformance', 'Good performance')}
+
diff --git a/options/components/providers/OpenAICompatibleProviderCard.jsx b/options/components/providers/OpenAICompatibleProviderCard.jsx
index 9892ac4..affb259 100644
--- a/options/components/providers/OpenAICompatibleProviderCard.jsx
+++ b/options/components/providers/OpenAICompatibleProviderCard.jsx
@@ -25,8 +25,14 @@ export function OpenAICompatibleProviderCard({
onModelChange,
onModelsLoaded,
}) {
- const { testResult, testing, fetchingModels, testConnection, fetchModels, initializeStatus } =
- useOpenAITest(t, fetchAvailableModels);
+ const {
+ testResult,
+ testing,
+ fetchingModels,
+ testConnection,
+ fetchModels,
+ initializeStatus,
+ } = useOpenAITest(t, fetchAvailableModels);
// Create debounced model fetching
const debouncedFetchRef = useRef(
@@ -38,7 +44,7 @@ export function OpenAICompatibleProviderCard({
// Initialize status
useEffect(() => {
initializeStatus(apiKey);
-
+
// Auto-fetch models if API key exists
if (apiKey) {
fetchModels(apiKey, baseUrl, onModelsLoaded);
@@ -68,7 +74,10 @@ export function OpenAICompatibleProviderCard({
return (
handleApiKeyChange(e.target.value)}
/>
@@ -94,7 +106,10 @@ export function OpenAICompatibleProviderCard({
handleBaseUrlChange(e.target.value)}
/>
@@ -117,7 +132,11 @@ export function OpenAICompatibleProviderCard({
))
) : (
- {fetchingModels ? 'Loading...' : 'No models available'}
+
+ {fetchingModels
+ ? 'Loading...'
+ : 'No models available'}
+
)}
@@ -130,8 +149,7 @@ export function OpenAICompatibleProviderCard({
>
{testing
? t('testingButton', 'Testing...')
- : t('testConnectionButton', 'Test Connection')
- }
+ : t('testConnectionButton', 'Test Connection')}
@@ -139,9 +157,21 @@ export function OpenAICompatibleProviderCard({
{t('providerFeatures', 'Features:')}
- {t('featureCustomizable', 'Customizable endpoint and model')}
- {t('featureApiKeyRequired', 'API key required')}
- {t('featureWideLanguageSupport', 'Wide language support')}
+
+ {t(
+ 'featureCustomizable',
+ 'Customizable endpoint and model'
+ )}
+
+
+ {t('featureApiKeyRequired', 'API key required')}
+
+
+ {t(
+ 'featureWideLanguageSupport',
+ 'Wide language support'
+ )}
+
diff --git a/options/components/providers/VertexProviderCard.jsx b/options/components/providers/VertexProviderCard.jsx
index d4f099f..19ee528 100644
--- a/options/components/providers/VertexProviderCard.jsx
+++ b/options/components/providers/VertexProviderCard.jsx
@@ -18,23 +18,28 @@ export function VertexProviderCard({
onProviderChange,
}) {
const fileInputRef = useRef(null);
- const {
- testResult,
- importResult,
- testing,
+ const {
+ testResult,
+ importResult,
+ testing,
importing,
- testConnection,
+ testConnection,
importServiceAccountJson,
refreshToken,
checkTokenExpiration,
- initializeStatus
- } = useVertexTest(t, onAccessTokenChange, onProjectIdChange, onProviderChange);
+ initializeStatus,
+ } = useVertexTest(
+ t,
+ onAccessTokenChange,
+ onProjectIdChange,
+ onProviderChange
+ );
// Initialize status and setup auto-refresh on mount
useEffect(() => {
const checkAndRefreshToken = async () => {
const expirationInfo = await checkTokenExpiration();
-
+
if (expirationInfo) {
// Auto-refresh if token is expired or will expire in less than 5 minutes
if (expirationInfo.isExpired || expirationInfo.shouldRefresh) {
@@ -44,7 +49,10 @@ export function VertexProviderCard({
console.log('[Vertex AI] Token auto-refreshed');
} catch (error) {
// Error already handled in hook
- console.error('[Vertex AI] Auto-refresh failed:', error);
+ console.error(
+ '[Vertex AI] Auto-refresh failed:',
+ error
+ );
}
}
}
@@ -55,12 +63,21 @@ export function VertexProviderCard({
checkAndRefreshToken();
// Setup periodic check every 5 minutes
- const interval = setInterval(() => {
- checkAndRefreshToken();
- }, 5 * 60 * 1000); // Check every 5 minutes
+ const interval = setInterval(
+ () => {
+ checkAndRefreshToken();
+ },
+ 5 * 60 * 1000
+ ); // Check every 5 minutes
return () => clearInterval(interval);
- }, [accessToken, projectId, initializeStatus, checkTokenExpiration, refreshToken]);
+ }, [
+ accessToken,
+ projectId,
+ initializeStatus,
+ checkTokenExpiration,
+ refreshToken,
+ ]);
const handleTest = () => {
const loc = location || 'us-central1';
@@ -103,7 +120,10 @@ export function VertexProviderCard({
return (
- {t('vertexServiceAccountLabel', 'Service Account JSON:')}
+
+ {t('vertexServiceAccountLabel', 'Service Account JSON:')}
+
{importing
? t('vertexImporting', 'Importing...')
- : t('vertexImportButton', 'Import JSON File')
- }
+ : t('vertexImportButton', 'Import JSON File')}
@@ -198,8 +219,7 @@ export function VertexProviderCard({
>
{testing
? t('testingButton', 'Testing...')
- : t('testConnectionButton', 'Test Connection')
- }
+ : t('testConnectionButton', 'Test Connection')}
@@ -208,14 +228,33 @@ export function VertexProviderCard({
{t('providerFeatures', 'Features:')}
- {t('featureVertexServiceAccount', 'Service account JSON import')}
- {t('featureVertexAutoToken', 'Automatic token generation')}
- {t('featureVertexGemini', 'Google Gemini models via Vertex AI')}
- {t('featureWideLanguageSupport', 'Wide language support')}
+
+ {t(
+ 'featureVertexServiceAccount',
+ 'Service account JSON import'
+ )}
+
+
+ {t(
+ 'featureVertexAutoToken',
+ 'Automatic token generation'
+ )}
+
+
+ {t(
+ 'featureVertexGemini',
+ 'Google Gemini models via Vertex AI'
+ )}
+
+
+ {t(
+ 'featureWideLanguageSupport',
+ 'Wide language support'
+ )}
+
);
}
-
diff --git a/options/components/sections/AIContextSection.jsx b/options/components/sections/AIContextSection.jsx
index 2c8abae..df04419 100644
--- a/options/components/sections/AIContextSection.jsx
+++ b/options/components/sections/AIContextSection.jsx
@@ -27,7 +27,7 @@ export function AIContextSection({ t, settings, onSettingChange }) {
const typesArray = Object.entries(newTypes)
.filter(([_, enabled]) => enabled)
.map(([type]) => type);
-
+
onSettingChange('aiContextTypes', typesArray);
};
@@ -37,10 +37,13 @@ export function AIContextSection({ t, settings, onSettingChange }) {
return (
{t('sectionAIContext', 'AI Context Assistant')}
-
+
{/* Card 1: Feature Toggle */}
- onSettingChange('aiContextProvider', e.target.value)
+ onSettingChange(
+ 'aiContextProvider',
+ e.target.value
+ )
}
>
OpenAI GPT
@@ -132,31 +138,34 @@ export function AIContextSection({ t, settings, onSettingChange }) {
onSettingChange('openaiModel', e.target.value)
}
>
-
GPT-4.1 Nano
-
GPT-4.1 Mini (Recommended)
-
GPT-4o Mini
-
GPT-4o
@@ -169,7 +178,10 @@ export function AIContextSection({ t, settings, onSettingChange }) {
{/* Card 4: Gemini Configuration */}
{aiContextEnabled && aiContextProvider === 'gemini' && (
-
Gemini 2.5 Flash (Recommended)
-
Gemini 2.5 Pro
-
Gemini 1.5 Flash
-
Gemini 1.5 Pro
@@ -254,7 +266,10 @@ export function AIContextSection({ t, settings, onSettingChange }) {
- {t('contextTypeHistoricalLabel', 'Historical Context:')}
+ {t(
+ 'contextTypeHistoricalLabel',
+ 'Historical Context:'
+ )}
- {t('contextTypeLinguisticLabel', 'Linguistic Context:')}
+ {t(
+ 'contextTypeLinguisticLabel',
+ 'Linguistic Context:'
+ )}
- {t('aiContextTimeoutLabel', 'Request Timeout (ms):')}
+ {t(
+ 'aiContextTimeoutLabel',
+ 'Request Timeout (ms):'
+ )}
- onSettingChange('aiContextTimeout', parseInt(e.target.value))
+ onSettingChange(
+ 'aiContextTimeout',
+ parseInt(e.target.value)
+ )
}
/>
- {t('aiContextRateLimitLabel', 'Rate Limit (requests/min):')}
+ {t(
+ 'aiContextRateLimitLabel',
+ 'Rate Limit (requests/min):'
+ )}
- onSettingChange('aiContextRateLimit', parseInt(e.target.value))
+ onSettingChange(
+ 'aiContextRateLimit',
+ parseInt(e.target.value)
+ )
}
/>
@@ -331,14 +361,20 @@ export function AIContextSection({ t, settings, onSettingChange }) {
id="aiContextCacheEnabled"
checked={settings.aiContextCacheEnabled || false}
onChange={(checked) =>
- onSettingChange('aiContextCacheEnabled', checked)
+ onSettingChange(
+ 'aiContextCacheEnabled',
+ checked
+ )
}
/>
- {t('aiContextRetryAttemptsLabel', 'Retry Attempts:')}
+ {t(
+ 'aiContextRetryAttemptsLabel',
+ 'Retry Attempts:'
+ )}
- onSettingChange('aiContextRetryAttempts', parseInt(e.target.value))
+ onSettingChange(
+ 'aiContextRetryAttempts',
+ parseInt(e.target.value)
+ )
}
/>
@@ -356,4 +395,4 @@ export function AIContextSection({ t, settings, onSettingChange }) {
)}
);
-}
\ No newline at end of file
+}
diff --git a/options/components/sections/AboutSection.jsx b/options/components/sections/AboutSection.jsx
index 8a58ab6..a059e39 100644
--- a/options/components/sections/AboutSection.jsx
+++ b/options/components/sections/AboutSection.jsx
@@ -23,9 +23,7 @@ export function AboutSection({ t }) {
'This extension helps you watch videos with dual language subtitles on various platforms.'
)}
-
- {t('aboutDevelopment', 'Developed by ')}{' '}
-
+ {t('aboutDevelopment', 'Developed by ')}
);
diff --git a/options/components/sections/GeneralSection.jsx b/options/components/sections/GeneralSection.jsx
index 469212d..e7f021f 100644
--- a/options/components/sections/GeneralSection.jsx
+++ b/options/components/sections/GeneralSection.jsx
@@ -6,7 +6,7 @@ export function GeneralSection({ t, settings, onSettingChange }) {
return (
{t('sectionGeneral', 'General')}
-
+
{
setOpenaiModels(models);
-
+
// Save the first model as default if no model is currently selected
if (models && models.length > 0) {
const savedModel = settings.openaiCompatibleModel;
const isValidModel = savedModel && models.includes(savedModel);
-
+
if (!isValidModel) {
// Use first model as default
await onSettingChange('openaiCompatibleModel', models[0]);
@@ -36,9 +36,7 @@ export function ProvidersSection({ t, settings, onSettingChange }) {
{t('sectionProviders', 'Provider Settings')}
- {selectedProvider === 'google' && (
-
- )}
+ {selectedProvider === 'google' && }
{selectedProvider === 'microsoft_edge_auth' && (
@@ -53,8 +51,12 @@ export function ProvidersSection({ t, settings, onSettingChange }) {
t={t}
apiKey={settings.deeplApiKey || ''}
apiPlan={settings.deeplApiPlan || 'free'}
- onApiKeyChange={(value) => onSettingChange('deeplApiKey', value)}
- onApiPlanChange={(value) => onSettingChange('deeplApiPlan', value)}
+ onApiKeyChange={(value) =>
+ onSettingChange('deeplApiKey', value)
+ }
+ onApiPlanChange={(value) =>
+ onSettingChange('deeplApiPlan', value)
+ }
/>
)}
@@ -65,9 +67,15 @@ export function ProvidersSection({ t, settings, onSettingChange }) {
baseUrl={settings.openaiCompatibleBaseUrl || ''}
model={settings.openaiCompatibleModel || ''}
models={openaiModels}
- onApiKeyChange={(value) => onSettingChange('openaiCompatibleApiKey', value)}
- onBaseUrlChange={(value) => onSettingChange('openaiCompatibleBaseUrl', value)}
- onModelChange={(value) => onSettingChange('openaiCompatibleModel', value)}
+ onApiKeyChange={(value) =>
+ onSettingChange('openaiCompatibleApiKey', value)
+ }
+ onBaseUrlChange={(value) =>
+ onSettingChange('openaiCompatibleBaseUrl', value)
+ }
+ onModelChange={(value) =>
+ onSettingChange('openaiCompatibleModel', value)
+ }
onModelsLoaded={handleOpenAIModelsLoaded}
/>
)}
@@ -79,11 +87,21 @@ export function ProvidersSection({ t, settings, onSettingChange }) {
projectId={settings.vertexProjectId || ''}
location={settings.vertexLocation || 'us-central1'}
model={settings.vertexModel || 'gemini-2.5-flash'}
- onAccessTokenChange={(value) => onSettingChange('vertexAccessToken', value)}
- onProjectIdChange={(value) => onSettingChange('vertexProjectId', value)}
- onLocationChange={(value) => onSettingChange('vertexLocation', value)}
- onModelChange={(value) => onSettingChange('vertexModel', value)}
- onProviderChange={(value) => onSettingChange('selectedProvider', value)}
+ onAccessTokenChange={(value) =>
+ onSettingChange('vertexAccessToken', value)
+ }
+ onProjectIdChange={(value) =>
+ onSettingChange('vertexProjectId', value)
+ }
+ onLocationChange={(value) =>
+ onSettingChange('vertexLocation', value)
+ }
+ onModelChange={(value) =>
+ onSettingChange('vertexModel', value)
+ }
+ onProviderChange={(value) =>
+ onSettingChange('selectedProvider', value)
+ }
/>
)}
diff --git a/options/components/sections/TranslationSection.jsx b/options/components/sections/TranslationSection.jsx
index b3dcef4..e4534f9 100644
--- a/options/components/sections/TranslationSection.jsx
+++ b/options/components/sections/TranslationSection.jsx
@@ -18,7 +18,7 @@ export function TranslationSection({ t, settings, onSettingChange }) {
return (
{t('sectionTranslation', 'Translation')}
-
+
- onSettingChange('translationBatchSize', parseInt(e.target.value))
+ onSettingChange(
+ 'translationBatchSize',
+ parseInt(e.target.value)
+ )
}
/>
@@ -84,7 +87,10 @@ export function TranslationSection({ t, settings, onSettingChange }) {
step="50"
value={settings.translationDelay || 150}
onChange={(e) =>
- onSettingChange('translationDelay', parseInt(e.target.value))
+ onSettingChange(
+ 'translationDelay',
+ parseInt(e.target.value)
+ )
}
/>
@@ -100,7 +106,10 @@ export function TranslationSection({ t, settings, onSettingChange }) {
- {t('batchingEnabledLabel', 'Enable Batch Translation:')}
+ {t(
+ 'batchingEnabledLabel',
+ 'Enable Batch Translation:'
+ )}
{t(
@@ -139,7 +148,10 @@ export function TranslationSection({ t, settings, onSettingChange }) {
id="useProviderDefaults"
checked={useProviderDefaults}
onChange={(checked) =>
- onSettingChange('useProviderDefaults', checked)
+ onSettingChange(
+ 'useProviderDefaults',
+ checked
+ )
}
/>
@@ -148,7 +160,10 @@ export function TranslationSection({ t, settings, onSettingChange }) {
- {t('globalBatchSizeLabel', 'Global Batch Size:')}
+ {t(
+ 'globalBatchSizeLabel',
+ 'Global Batch Size:'
+ )}
{t(
@@ -165,7 +180,10 @@ export function TranslationSection({ t, settings, onSettingChange }) {
step="1"
value={settings.globalBatchSize || 5}
onChange={(e) =>
- onSettingChange('globalBatchSize', parseInt(e.target.value))
+ onSettingChange(
+ 'globalBatchSize',
+ parseInt(e.target.value)
+ )
}
/>
@@ -174,7 +192,10 @@ export function TranslationSection({ t, settings, onSettingChange }) {
- {t('smartBatchingLabel', 'Smart Batch Optimization:')}
+ {t(
+ 'smartBatchingLabel',
+ 'Smart Batch Optimization:'
+ )}
{t(
@@ -195,7 +216,10 @@ export function TranslationSection({ t, settings, onSettingChange }) {
- {t('maxConcurrentBatchesLabel', 'Maximum Concurrent Batches:')}
+ {t(
+ 'maxConcurrentBatchesLabel',
+ 'Maximum Concurrent Batches:'
+ )}
{t(
@@ -212,7 +236,10 @@ export function TranslationSection({ t, settings, onSettingChange }) {
step="1"
value={settings.maxConcurrentBatches || 2}
onChange={(e) =>
- onSettingChange('maxConcurrentBatches', parseInt(e.target.value))
+ onSettingChange(
+ 'maxConcurrentBatches',
+ parseInt(e.target.value)
+ )
}
/>
@@ -222,7 +249,10 @@ export function TranslationSection({ t, settings, onSettingChange }) {
{batchingEnabled && useProviderDefaults && (
- {t('openaieBatchSizeLabel', 'OpenAI Batch Size:')}
+ {t(
+ 'openaieBatchSizeLabel',
+ 'OpenAI Batch Size:'
+ )}
- {t('openaieBatchSizeHelp', 'Recommended: 5-10 segments (default: 8)')}
+ {t(
+ 'openaieBatchSizeHelp',
+ 'Recommended: 5-10 segments (default: 8)'
+ )}
- onSettingChange('openaieBatchSize', parseInt(e.target.value))
+ onSettingChange(
+ 'openaieBatchSize',
+ parseInt(e.target.value)
+ )
}
/>
@@ -253,10 +292,16 @@ export function TranslationSection({ t, settings, onSettingChange }) {
@@ -278,7 +326,10 @@ export function TranslationSection({ t, settings, onSettingChange }) {
{t('deeplBatchSizeLabel', 'DeepL Batch Size:')}
- {t('deeplBatchSizeHelp', 'Recommended: 2-3 segments (default: 3)')}
+ {t(
+ 'deeplBatchSizeHelp',
+ 'Recommended: 2-3 segments (default: 3)'
+ )}
- onSettingChange('deeplBatchSize', parseInt(e.target.value))
+ onSettingChange(
+ 'deeplBatchSize',
+ parseInt(e.target.value)
+ )
}
/>
@@ -297,10 +351,16 @@ export function TranslationSection({ t, settings, onSettingChange }) {
@@ -319,10 +382,16 @@ export function TranslationSection({ t, settings, onSettingChange }) {
@@ -341,7 +413,10 @@ export function TranslationSection({ t, settings, onSettingChange }) {
)}
- {t('openaieDelayLabel', 'OpenAI Request Delay (ms):')}
+ {t(
+ 'openaieDelayLabel',
+ 'OpenAI Request Delay (ms):'
+ )}
- {t('openaieDelayHelp', 'Minimum delay between requests (default: 100ms)')}
+ {t(
+ 'openaieDelayHelp',
+ 'Minimum delay between requests (default: 100ms)'
+ )}
- onSettingChange('openaieDelay', parseInt(e.target.value))
+ onSettingChange(
+ 'openaieDelay',
+ parseInt(e.target.value)
+ )
}
/>
@@ -372,10 +456,16 @@ export function TranslationSection({ t, settings, onSettingChange }) {
@@ -394,10 +487,16 @@ export function TranslationSection({ t, settings, onSettingChange }) {
@@ -416,10 +518,16 @@ export function TranslationSection({ t, settings, onSettingChange }) {
@@ -438,10 +549,16 @@ export function TranslationSection({ t, settings, onSettingChange }) {
@@ -460,10 +580,16 @@ export function TranslationSection({ t, settings, onSettingChange }) {
diff --git a/options/hooks/useBackgroundReady.js b/options/hooks/useBackgroundReady.js
index cb9dd78..64b5769 100644
--- a/options/hooks/useBackgroundReady.js
+++ b/options/hooks/useBackgroundReady.js
@@ -20,30 +20,38 @@ export function useBackgroundReady() {
}
}, []);
- const waitForBackgroundReady = useCallback(async (maxRetries = 10, delay = 500) => {
- setChecking(true);
-
- for (let i = 0; i < maxRetries; i++) {
- if (await checkBackgroundReady()) {
- console.debug('Background script is ready', { attempt: i + 1 });
- setIsReady(true);
- setChecking(false);
- return true;
+ const waitForBackgroundReady = useCallback(
+ async (maxRetries = 10, delay = 500) => {
+ setChecking(true);
+
+ for (let i = 0; i < maxRetries; i++) {
+ if (await checkBackgroundReady()) {
+ console.debug('Background script is ready', {
+ attempt: i + 1,
+ });
+ setIsReady(true);
+ setChecking(false);
+ return true;
+ }
+ console.debug('Background script not ready, retrying...', {
+ attempt: i + 1,
+ maxRetries,
+ });
+ await new Promise((resolve) => setTimeout(resolve, delay));
}
- console.debug('Background script not ready, retrying...', {
- attempt: i + 1,
- maxRetries,
- });
- await new Promise((resolve) => setTimeout(resolve, delay));
- }
-
- console.warn('Background script did not become ready within timeout', {
- maxRetries,
- totalWaitTime: maxRetries * delay,
- });
- setChecking(false);
- return false;
- }, [checkBackgroundReady]);
+
+ console.warn(
+ 'Background script did not become ready within timeout',
+ {
+ maxRetries,
+ totalWaitTime: maxRetries * delay,
+ }
+ );
+ setChecking(false);
+ return false;
+ },
+ [checkBackgroundReady]
+ );
return {
isReady,
diff --git a/options/hooks/useDeepLTest.js b/options/hooks/useDeepLTest.js
index 8361d38..4e4ba07 100644
--- a/options/hooks/useDeepLTest.js
+++ b/options/hooks/useDeepLTest.js
@@ -21,95 +21,150 @@ export function useDeepLTest(t) {
});
}, []);
- const testConnection = useCallback(async (apiKey, apiPlan) => {
- if (
- typeof window.DeepLAPI === 'undefined' ||
- !window.DeepLAPI ||
- typeof window.DeepLAPI.testDeepLConnection !== 'function'
- ) {
- showTestResult(
- t('deeplApiNotLoadedError', '❌ DeepL API script is not available. Please refresh the page.'),
- 'error'
- );
- return;
- }
+ const testConnection = useCallback(
+ async (apiKey, apiPlan) => {
+ if (
+ typeof window.DeepLAPI === 'undefined' ||
+ !window.DeepLAPI ||
+ typeof window.DeepLAPI.testDeepLConnection !== 'function'
+ ) {
+ showTestResult(
+ t(
+ 'deeplApiNotLoadedError',
+ '❌ DeepL API script is not available. Please refresh the page.'
+ ),
+ 'error'
+ );
+ return;
+ }
+
+ if (!apiKey) {
+ showTestResult(
+ t(
+ 'deeplApiKeyError',
+ 'Please enter your DeepL API key first.'
+ ),
+ 'error'
+ );
+ return;
+ }
- if (!apiKey) {
+ setTesting(true);
showTestResult(
- t('deeplApiKeyError', 'Please enter your DeepL API key first.'),
- 'error'
+ t('testingConnection', 'Testing DeepL connection...'),
+ 'info'
);
- return;
- }
- setTesting(true);
- showTestResult(
- t('testingConnection', 'Testing DeepL connection...'),
- 'info'
- );
+ try {
+ const result = await window.DeepLAPI.testDeepLConnection(
+ apiKey,
+ apiPlan
+ );
- try {
- const result = await window.DeepLAPI.testDeepLConnection(apiKey, apiPlan);
+ if (result.success) {
+ showTestResult(
+ t(
+ 'deeplTestSuccessSimple',
+ '✅ DeepL API test successful!'
+ ),
+ 'success'
+ );
+ } else {
+ let fallbackMessage;
- if (result.success) {
- showTestResult(
- t('deeplTestSuccessSimple', '✅ DeepL API test successful!'),
- 'success'
- );
- } else {
- let fallbackMessage;
+ switch (result.error) {
+ case 'API_KEY_MISSING':
+ fallbackMessage = t(
+ 'deeplApiKeyError',
+ 'Please enter your DeepL API key first.'
+ );
+ break;
+ case 'UNEXPECTED_FORMAT':
+ fallbackMessage = t(
+ 'deeplTestUnexpectedFormat',
+ '⚠️ DeepL API responded but with unexpected format'
+ );
+ break;
+ case 'HTTP_403':
+ fallbackMessage = t(
+ 'deeplTestInvalidKey',
+ '❌ DeepL API key is invalid or has been rejected.'
+ );
+ break;
+ case 'HTTP_456':
+ fallbackMessage = t(
+ 'deeplTestQuotaExceeded',
+ '❌ DeepL API quota exceeded. Please check your usage limits.'
+ );
+ break;
+ case 'NETWORK_ERROR':
+ fallbackMessage = t(
+ 'deeplTestNetworkError',
+ '❌ Network error: Could not connect to DeepL API. Check your internet connection.'
+ );
+ break;
+ default:
+ if (result.error.startsWith('HTTP_')) {
+ fallbackMessage = t(
+ 'deeplTestApiError',
+ '❌ DeepL API error (%d): %s',
+ result.status,
+ result.message || 'Unknown error'
+ );
+ } else {
+ fallbackMessage = t(
+ 'deeplTestGenericError',
+ '❌ Test failed: %s',
+ result.message
+ );
+ }
+ break;
+ }
- switch (result.error) {
- case 'API_KEY_MISSING':
- fallbackMessage = t('deeplApiKeyError', 'Please enter your DeepL API key first.');
- break;
- case 'UNEXPECTED_FORMAT':
- fallbackMessage = t('deeplTestUnexpectedFormat', '⚠️ DeepL API responded but with unexpected format');
- break;
- case 'HTTP_403':
- fallbackMessage = t('deeplTestInvalidKey', '❌ DeepL API key is invalid or has been rejected.');
- break;
- case 'HTTP_456':
- fallbackMessage = t('deeplTestQuotaExceeded', '❌ DeepL API quota exceeded. Please check your usage limits.');
- break;
- case 'NETWORK_ERROR':
- fallbackMessage = t('deeplTestNetworkError', '❌ Network error: Could not connect to DeepL API. Check your internet connection.');
- break;
- default:
- if (result.error.startsWith('HTTP_')) {
- fallbackMessage = t('deeplTestApiError', '❌ DeepL API error (%d): %s', result.status, result.message || 'Unknown error');
- } else {
- fallbackMessage = t('deeplTestGenericError', '❌ Test failed: %s', result.message);
- }
- break;
+ const errorType =
+ result.error === 'UNEXPECTED_FORMAT'
+ ? 'warning'
+ : 'error';
+ showTestResult(fallbackMessage, errorType);
}
-
- const errorType = result.error === 'UNEXPECTED_FORMAT' ? 'warning' : 'error';
- showTestResult(fallbackMessage, errorType);
+ } catch (error) {
+ showTestResult(
+ t(
+ 'deeplTestGenericError',
+ '❌ Test failed: %s',
+ error.message
+ ),
+ 'error'
+ );
+ } finally {
+ setTesting(false);
}
- } catch (error) {
- showTestResult(
- t('deeplTestGenericError', '❌ Test failed: %s', error.message),
- 'error'
- );
- } finally {
- setTesting(false);
- }
- }, [t, showTestResult]);
+ },
+ [t, showTestResult]
+ );
- const initializeStatus = useCallback((apiKey) => {
- if (apiKey) {
- showTestResult(
- t('deeplTestNeedsTesting', '⚠️ DeepL API key needs testing.'),
- 'warning'
- );
- } else {
- showTestResult(
- t('deeplApiKeyError', 'Please enter your DeepL API key first.'),
- 'error'
- );
- }
- }, [t, showTestResult]);
+ const initializeStatus = useCallback(
+ (apiKey) => {
+ if (apiKey) {
+ showTestResult(
+ t(
+ 'deeplTestNeedsTesting',
+ '⚠️ DeepL API key needs testing.'
+ ),
+ 'warning'
+ );
+ } else {
+ showTestResult(
+ t(
+ 'deeplApiKeyError',
+ 'Please enter your DeepL API key first.'
+ ),
+ 'error'
+ );
+ }
+ },
+ [t, showTestResult]
+ );
return {
testResult,
diff --git a/options/hooks/useOpenAITest.js b/options/hooks/useOpenAITest.js
index 92c837f..2b4c71d 100644
--- a/options/hooks/useOpenAITest.js
+++ b/options/hooks/useOpenAITest.js
@@ -23,82 +23,105 @@ export function useOpenAITest(t, fetchAvailableModels) {
});
}, []);
- const testConnection = useCallback(async (apiKey, baseUrl) => {
- if (!apiKey) {
+ const testConnection = useCallback(
+ async (apiKey, baseUrl) => {
+ if (!apiKey) {
+ showTestResult(
+ t('openaiApiKeyError', 'Please enter an API key first.'),
+ 'error'
+ );
+ return;
+ }
+
+ setTesting(true);
showTestResult(
- t('openaiApiKeyError', 'Please enter an API key first.'),
- 'error'
+ t('openaiTestingConnection', 'Testing connection...'),
+ 'info'
);
- return;
- }
- setTesting(true);
- showTestResult(
- t('openaiTestingConnection', 'Testing connection...'),
- 'info'
- );
+ try {
+ await fetchAvailableModels(apiKey, baseUrl);
+ showTestResult(
+ t('openaiConnectionSuccessful', 'Connection successful!'),
+ 'success'
+ );
+ } catch (error) {
+ showTestResult(
+ t(
+ 'openaiConnectionFailed',
+ 'Connection failed: %s',
+ error.message
+ ),
+ 'error'
+ );
+ } finally {
+ setTesting(false);
+ }
+ },
+ [t, fetchAvailableModels, showTestResult]
+ );
- try {
- await fetchAvailableModels(apiKey, baseUrl);
- showTestResult(
- t('openaiConnectionSuccessful', 'Connection successful!'),
- 'success'
- );
- } catch (error) {
+ const fetchModels = useCallback(
+ async (apiKey, baseUrl, onModelsLoaded) => {
+ if (!apiKey) {
+ return;
+ }
+
+ setFetchingModels(true);
showTestResult(
- t('openaiConnectionFailed', 'Connection failed: %s', error.message),
- 'error'
+ t('openaieFetchingModels', 'Fetching models...'),
+ 'info'
);
- } finally {
- setTesting(false);
- }
- }, [t, fetchAvailableModels, showTestResult]);
- const fetchModels = useCallback(async (apiKey, baseUrl, onModelsLoaded) => {
- if (!apiKey) {
- return;
- }
+ try {
+ const models = await fetchAvailableModels(apiKey, baseUrl);
- setFetchingModels(true);
- showTestResult(
- t('openaieFetchingModels', 'Fetching models...'),
- 'info'
- );
+ if (onModelsLoaded) {
+ onModelsLoaded(models);
+ }
- try {
- const models = await fetchAvailableModels(apiKey, baseUrl);
-
- if (onModelsLoaded) {
- onModelsLoaded(models);
+ showTestResult(
+ t(
+ 'openaiModelsFetchedSuccessfully',
+ 'Models fetched successfully.'
+ ),
+ 'success'
+ );
+ } catch (error) {
+ showTestResult(
+ t(
+ 'openaiFailedToFetchModels',
+ 'Failed to fetch models: %s',
+ error.message
+ ),
+ 'error'
+ );
+ } finally {
+ setFetchingModels(false);
}
+ },
+ [t, fetchAvailableModels, showTestResult]
+ );
- showTestResult(
- t('openaiModelsFetchedSuccessfully', 'Models fetched successfully.'),
- 'success'
- );
- } catch (error) {
- showTestResult(
- t('openaiFailedToFetchModels', 'Failed to fetch models: %s', error.message),
- 'error'
- );
- } finally {
- setFetchingModels(false);
- }
- }, [t, fetchAvailableModels, showTestResult]);
-
- const initializeStatus = useCallback((apiKey) => {
- if (apiKey) {
- showTestResult(
- t('openaiTestNeedsTesting', '⚠️ OpenAI-compatible API key needs testing.'),
- 'warning'
- );
- } else {
- showTestResult(
- t('openaiApiKeyError', 'Please enter your API key first.'),
- 'error'
- );
- }
- }, [t, showTestResult]);
+ const initializeStatus = useCallback(
+ (apiKey) => {
+ if (apiKey) {
+ showTestResult(
+ t(
+ 'openaiTestNeedsTesting',
+ '⚠️ OpenAI-compatible API key needs testing.'
+ ),
+ 'warning'
+ );
+ } else {
+ showTestResult(
+ t('openaiApiKeyError', 'Please enter your API key first.'),
+ 'error'
+ );
+ }
+ },
+ [t, showTestResult]
+ );
return {
testResult,
diff --git a/options/hooks/useVertexTest.js b/options/hooks/useVertexTest.js
index 0ed7be7..fbd5ddd 100644
--- a/options/hooks/useVertexTest.js
+++ b/options/hooks/useVertexTest.js
@@ -1,5 +1,8 @@
import { useState, useCallback } from 'react';
-import { getAccessTokenFromServiceAccount, checkTokenExpiration as checkExpiration } from '../../utils/vertexAuth.js';
+import {
+ getAccessTokenFromServiceAccount,
+ checkTokenExpiration as checkExpiration,
+} from '../../utils/vertexAuth.js';
/**
* Hook for testing Vertex AI and importing service account JSON
@@ -9,7 +12,12 @@ import { getAccessTokenFromServiceAccount, checkTokenExpiration as checkExpirati
* @param {Function} onProviderChange - Callback to switch provider
* @returns {Object} Test functions and state
*/
-export function useVertexTest(t, onAccessTokenChange, onProjectIdChange, onProviderChange) {
+export function useVertexTest(
+ t,
+ onAccessTokenChange,
+ onProjectIdChange,
+ onProviderChange
+) {
const [testResult, setTestResult] = useState({
visible: false,
message: '',
@@ -39,235 +47,307 @@ export function useVertexTest(t, onAccessTokenChange, onProjectIdChange, onProvi
});
}, []);
- const testConnection = useCallback(async (accessToken, projectId, location, model) => {
- if (!accessToken || !projectId) {
- showTestResult(
- t('vertexMissingConfig', 'Please enter access token and project ID.'),
- 'error'
- );
- return;
- }
-
- setTesting(true);
- showTestResult(
- t('openaiTestingConnection', 'Testing connection...'),
- 'info'
- );
-
- try {
- const normalizedModel = model.startsWith('models/') ? model.split('/').pop() : model;
- const endpoint = `https://${location}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${location}/publishers/google/models/${normalizedModel}:generateContent`;
-
- const body = {
- contents: [{ role: 'user', parts: [{ text: 'ping' }] }],
- generationConfig: { temperature: 0 },
- };
- const res = await fetch(endpoint, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: `Bearer ${accessToken}`,
- },
- body: JSON.stringify(body),
- });
-
- if (!res.ok) {
- const text = await res.text();
- throw new Error(`${res.status} ${res.statusText}: ${text}`);
+ const testConnection = useCallback(
+ async (accessToken, projectId, location, model) => {
+ if (!accessToken || !projectId) {
+ showTestResult(
+ t(
+ 'vertexMissingConfig',
+ 'Please enter access token and project ID.'
+ ),
+ 'error'
+ );
+ return;
}
+ setTesting(true);
showTestResult(
- t('openaiConnectionSuccessful', 'Connection successful!'),
- 'success'
- );
- } catch (error) {
- showTestResult(
- t('vertexConnectionFailed', 'Connection failed: %s', error.message),
- 'error'
- );
- } finally {
- setTesting(false);
- }
- }, [t, showTestResult]);
-
- const importServiceAccountJson = useCallback(async (file) => {
- if (!file) return;
-
- setImporting(true);
- showImportResult(
- t('vertexImporting', 'Importing service account...'),
- 'info'
- );
-
- try {
- const text = await file.text();
- let sa;
- try {
- sa = JSON.parse(text);
- } catch (e) {
- throw new Error('Invalid JSON file.');
- }
-
- const required = ['type', 'project_id', 'private_key', 'client_email'];
- const missing = required.filter((k) => !sa[k] || typeof sa[k] !== 'string' || sa[k].trim() === '');
- if (missing.length > 0) {
- throw new Error(`Missing fields: ${missing.join(', ')}`);
- }
- if (sa.type !== 'service_account') {
- throw new Error('JSON is not a service account key.');
- }
-
- showImportResult(
- t('vertexGeneratingToken', 'Generating access token...'),
+ t('openaiTestingConnection', 'Testing connection...'),
'info'
);
- const { accessToken, expiresIn } = await getAccessTokenFromServiceAccount(sa);
-
- // Calculate token expiration time
- const expiresAt = Date.now() + (expiresIn * 1000);
-
- // Store the service account JSON for auto-refresh
- // Security Note: Storing the complete service account (including private_key) in
- // chrome.storage.local is a security trade-off to enable automatic token refresh.
- // Chrome extension storage is isolated per-extension and encrypted at rest by the OS.
- // Alternative approaches (e.g., storing only the token) would require manual
- // re-import every hour when tokens expire. Users with high security requirements
- // should use short-lived tokens and manual refresh instead of storing credentials.
- if (typeof chrome !== 'undefined' && chrome.storage) {
- await chrome.storage.local.set({
- vertexServiceAccount: sa,
- vertexTokenExpiresAt: expiresAt,
- });
- }
- // Update settings via callbacks
- await onProjectIdChange(sa.project_id);
- await onAccessTokenChange(accessToken);
+ try {
+ const normalizedModel = model.startsWith('models/')
+ ? model.split('/').pop()
+ : model;
+ const endpoint = `https://${location}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${location}/publishers/google/models/${normalizedModel}:generateContent`;
+
+ const body = {
+ contents: [{ role: 'user', parts: [{ text: 'ping' }] }],
+ generationConfig: { temperature: 0 },
+ };
+ const res = await fetch(endpoint, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${accessToken}`,
+ },
+ body: JSON.stringify(body),
+ });
- showImportResult(
- '✅ ' + t('vertexImportSuccess', 'Service account imported and token generated.'),
- 'success'
- );
+ if (!res.ok) {
+ const text = await res.text();
+ throw new Error(`${res.status} ${res.statusText}: ${text}`);
+ }
- // Switch provider to Vertex
- if (onProviderChange) {
- await onProviderChange('vertex_gemini');
+ showTestResult(
+ t('openaiConnectionSuccessful', 'Connection successful!'),
+ 'success'
+ );
+ } catch (error) {
+ showTestResult(
+ t(
+ 'vertexConnectionFailed',
+ 'Connection failed: %s',
+ error.message
+ ),
+ 'error'
+ );
+ } finally {
+ setTesting(false);
}
+ },
+ [t, showTestResult]
+ );
+
+ const importServiceAccountJson = useCallback(
+ async (file) => {
+ if (!file) return;
- return { projectId: sa.project_id, accessToken, expiresAt };
- } catch (error) {
- showImportResult(
- t('vertexImportFailed', 'Import failed: %s', error.message),
- 'error'
- );
- throw error;
- } finally {
- setImporting(false);
- }
- }, [t, showImportResult, onAccessTokenChange, onProjectIdChange, onProviderChange]);
-
- const refreshToken = useCallback(async (silent = false) => {
- if (!silent) {
setImporting(true);
showImportResult(
- t('vertexRefreshingToken', 'Refreshing access token...'),
+ t('vertexImporting', 'Importing service account...'),
'info'
);
- }
-
- try {
- // Retrieve stored service account
- if (typeof chrome === 'undefined' || !chrome.storage) {
- throw new Error('Chrome storage not available');
- }
- const result = await chrome.storage.local.get(['vertexServiceAccount']);
- const sa = result.vertexServiceAccount;
-
- if (!sa) {
- throw new Error('No stored service account found. Please import the JSON file again.');
- }
-
- // Generate new token
- const { accessToken, expiresIn } = await getAccessTokenFromServiceAccount(sa);
+ try {
+ const text = await file.text();
+ let sa;
+ try {
+ sa = JSON.parse(text);
+ } catch (e) {
+ throw new Error('Invalid JSON file.');
+ }
- // Calculate new expiration time
- const expiresAt = Date.now() + (expiresIn * 1000);
+ const required = [
+ 'type',
+ 'project_id',
+ 'private_key',
+ 'client_email',
+ ];
+ const missing = required.filter(
+ (k) =>
+ !sa[k] ||
+ typeof sa[k] !== 'string' ||
+ sa[k].trim() === ''
+ );
+ if (missing.length > 0) {
+ throw new Error(`Missing fields: ${missing.join(', ')}`);
+ }
+ if (sa.type !== 'service_account') {
+ throw new Error('JSON is not a service account key.');
+ }
- // Update expiration time in storage
- await chrome.storage.local.set({
- vertexTokenExpiresAt: expiresAt,
- });
+ showImportResult(
+ t('vertexGeneratingToken', 'Generating access token...'),
+ 'info'
+ );
+ const { accessToken, expiresIn } =
+ await getAccessTokenFromServiceAccount(sa);
+
+ // Calculate token expiration time
+ const expiresAt = Date.now() + expiresIn * 1000;
+
+ // Store the service account JSON for auto-refresh
+ // Security Note: Storing the complete service account (including private_key) in
+ // chrome.storage.local is a security trade-off to enable automatic token refresh.
+ // Chrome extension storage is isolated per-extension and encrypted at rest by the OS.
+ // Alternative approaches (e.g., storing only the token) would require manual
+ // re-import every hour when tokens expire. Users with high security requirements
+ // should use short-lived tokens and manual refresh instead of storing credentials.
+ if (typeof chrome !== 'undefined' && chrome.storage) {
+ await chrome.storage.local.set({
+ vertexServiceAccount: sa,
+ vertexTokenExpiresAt: expiresAt,
+ });
+ }
- // Update settings via callback
- await onAccessTokenChange(accessToken);
+ // Update settings via callbacks
+ await onProjectIdChange(sa.project_id);
+ await onAccessTokenChange(accessToken);
- if (!silent) {
showImportResult(
- '✅ ' + t('vertexTokenRefreshed', 'Access token refreshed successfully.'),
+ '✅ ' +
+ t(
+ 'vertexImportSuccess',
+ 'Service account imported and token generated.'
+ ),
'success'
);
- } else {
- console.log('[Vertex AI] Access token auto-refreshed successfully');
- }
- return { accessToken, expiresAt };
- } catch (error) {
- if (!silent) {
+ // Switch provider to Vertex
+ if (onProviderChange) {
+ await onProviderChange('vertex_gemini');
+ }
+
+ return { projectId: sa.project_id, accessToken, expiresAt };
+ } catch (error) {
showImportResult(
- t('vertexRefreshFailed', 'Token refresh failed: %s', error.message),
+ t('vertexImportFailed', 'Import failed: %s', error.message),
'error'
);
- } else {
- console.error('[Vertex AI] Auto-refresh failed:', error);
+ throw error;
+ } finally {
+ setImporting(false);
}
- throw error;
- } finally {
+ },
+ [
+ t,
+ showImportResult,
+ onAccessTokenChange,
+ onProjectIdChange,
+ onProviderChange,
+ ]
+ );
+
+ const refreshToken = useCallback(
+ async (silent = false) => {
if (!silent) {
- setImporting(false);
+ setImporting(true);
+ showImportResult(
+ t('vertexRefreshingToken', 'Refreshing access token...'),
+ 'info'
+ );
}
- }
- }, [t, showImportResult, onAccessTokenChange]);
+
+ try {
+ // Retrieve stored service account
+ if (typeof chrome === 'undefined' || !chrome.storage) {
+ throw new Error('Chrome storage not available');
+ }
+
+ const result = await chrome.storage.local.get([
+ 'vertexServiceAccount',
+ ]);
+ const sa = result.vertexServiceAccount;
+
+ if (!sa) {
+ throw new Error(
+ 'No stored service account found. Please import the JSON file again.'
+ );
+ }
+
+ // Generate new token
+ const { accessToken, expiresIn } =
+ await getAccessTokenFromServiceAccount(sa);
+
+ // Calculate new expiration time
+ const expiresAt = Date.now() + expiresIn * 1000;
+
+ // Update expiration time in storage
+ await chrome.storage.local.set({
+ vertexTokenExpiresAt: expiresAt,
+ });
+
+ // Update settings via callback
+ await onAccessTokenChange(accessToken);
+
+ if (!silent) {
+ showImportResult(
+ '✅ ' +
+ t(
+ 'vertexTokenRefreshed',
+ 'Access token refreshed successfully.'
+ ),
+ 'success'
+ );
+ } else {
+ console.log(
+ '[Vertex AI] Access token auto-refreshed successfully'
+ );
+ }
+
+ return { accessToken, expiresAt };
+ } catch (error) {
+ if (!silent) {
+ showImportResult(
+ t(
+ 'vertexRefreshFailed',
+ 'Token refresh failed: %s',
+ error.message
+ ),
+ 'error'
+ );
+ } else {
+ console.error('[Vertex AI] Auto-refresh failed:', error);
+ }
+ throw error;
+ } finally {
+ if (!silent) {
+ setImporting(false);
+ }
+ }
+ },
+ [t, showImportResult, onAccessTokenChange]
+ );
const checkTokenExpiration = useCallback(async () => {
return await checkExpiration();
}, []);
- const initializeStatus = useCallback(async (accessToken, projectId) => {
- if (accessToken && projectId) {
- // Check if token is about to expire
- const expirationInfo = await checkTokenExpiration();
-
- if (expirationInfo) {
- if (expirationInfo.isExpired) {
- showTestResult(
- t('vertexTokenExpired', '⚠️ Access token expired. Click refresh to renew.'),
- 'warning'
- );
- } else if (expirationInfo.shouldRefresh) {
- showTestResult(
- t('vertexTokenExpiringSoon', `⚠️ Token expires in ${expirationInfo.expiresInMinutes} minutes. Consider refreshing.`),
- 'warning'
- );
+ const initializeStatus = useCallback(
+ async (accessToken, projectId) => {
+ if (accessToken && projectId) {
+ // Check if token is about to expire
+ const expirationInfo = await checkTokenExpiration();
+
+ if (expirationInfo) {
+ if (expirationInfo.isExpired) {
+ showTestResult(
+ t(
+ 'vertexTokenExpired',
+ '⚠️ Access token expired. Click refresh to renew.'
+ ),
+ 'warning'
+ );
+ } else if (expirationInfo.shouldRefresh) {
+ showTestResult(
+ t(
+ 'vertexTokenExpiringSoon',
+ `⚠️ Token expires in ${expirationInfo.expiresInMinutes} minutes. Consider refreshing.`
+ ),
+ 'warning'
+ );
+ } else {
+ showTestResult(
+ t(
+ 'vertexConfigured',
+ '⚠️ Vertex AI configured. Please test connection.'
+ ),
+ 'warning'
+ );
+ }
} else {
showTestResult(
- t('vertexConfigured', '⚠️ Vertex AI configured. Please test connection.'),
+ t(
+ 'vertexConfigured',
+ '⚠️ Vertex AI configured. Please test connection.'
+ ),
'warning'
);
}
} else {
showTestResult(
- t('vertexConfigured', '⚠️ Vertex AI configured. Please test connection.'),
- 'warning'
+ t(
+ 'vertexNotConfigured',
+ 'Please import service account JSON or enter credentials.'
+ ),
+ 'error'
);
}
- } else {
- showTestResult(
- t('vertexNotConfigured', 'Please import service account JSON or enter credentials.'),
- 'error'
- );
- }
- }, [t, showTestResult, checkTokenExpiration]);
+ },
+ [t, showTestResult, checkTokenExpiration]
+ );
return {
testResult,
@@ -283,4 +363,3 @@ export function useVertexTest(t, onAccessTokenChange, onProjectIdChange, onProvi
showImportResult,
};
}
-
diff --git a/options/options.css b/options/options.css
index f849b98..f68a84b 100644
--- a/options/options.css
+++ b/options/options.css
@@ -740,12 +740,14 @@ button#testDeepLButton.btn-sparkle:hover span.text {
border-radius: 8px;
background: linear-gradient(180deg, #007aff 0%, #0051d5 100%);
color: white;
- font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', sans-serif;
+ font-family:
+ -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue',
+ sans-serif;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);
- box-shadow:
+ box-shadow:
0 1px 2px rgba(0, 122, 255, 0.3),
0 2px 4px rgba(0, 0, 0, 0.1);
position: relative;
@@ -759,13 +761,17 @@ button#testDeepLButton.btn-sparkle:hover span.text {
left: 0;
right: 0;
height: 50%;
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0) 100%);
+ background: linear-gradient(
+ 180deg,
+ rgba(255, 255, 255, 0.15) 0%,
+ rgba(255, 255, 255, 0) 100%
+ );
pointer-events: none;
}
.apple-file-btn:hover {
background: linear-gradient(180deg, #0077ed 0%, #004fc7 100%);
- box-shadow:
+ box-shadow:
0 2px 4px rgba(0, 122, 255, 0.4),
0 4px 8px rgba(0, 0, 0, 0.15);
transform: translateY(-1px);
@@ -773,7 +779,7 @@ button#testDeepLButton.btn-sparkle:hover span.text {
.apple-file-btn:active {
background: linear-gradient(180deg, #0051d5 0%, #003fa8 100%);
- box-shadow:
+ box-shadow:
0 1px 2px rgba(0, 122, 255, 0.3),
0 1px 3px rgba(0, 0, 0, 0.2);
transform: translateY(0);
@@ -782,8 +788,7 @@ button#testDeepLButton.btn-sparkle:hover span.text {
.apple-file-btn:disabled {
background: linear-gradient(180deg, #c7c7cc 0%, #aeaeb2 100%);
cursor: not-allowed;
- box-shadow:
- 0 1px 2px rgba(0, 0, 0, 0.1);
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.apple-file-btn:disabled::before {
diff --git a/options/options.html b/options/options.html
index 97971c2..3bee63c 100644
--- a/options/options.html
+++ b/options/options.html
@@ -9,4 +9,4 @@