From 98853982de7dfa5619e421d04715b20c8bd01d75 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 21 Oct 2025 22:02:19 +0000 Subject: [PATCH] style: Auto-format and lint code --- CHANGELOG.md | 57 ++- README_zh.md | 2 +- _locales/en/messages.json | 40 +- _locales/es/messages.json | 52 +- _locales/ja/messages.json | 44 +- _locales/ko/messages.json | 36 +- _locales/zh_CN/messages.json | 16 +- _locales/zh_TW/messages.json | 20 +- background/services/translationService.js | 12 +- config/configSchema.js | 12 +- content_scripts/core/BaseContentScript.js | 14 +- docs/en/installation.md | 16 +- docs/zh/installation.md | 14 +- options/OptionsApp.jsx | 2 +- options/components/AppleStyleFileButton.jsx | 47 +- options/components/SparkleButton.jsx | 7 +- options/components/TestResultDisplay.jsx | 6 +- .../providers/DeepLFreeProviderCard.jsx | 42 +- .../providers/DeepLProviderCard.jsx | 34 +- .../providers/GoogleProviderCard.jsx | 11 +- .../providers/MicrosoftProviderCard.jsx | 11 +- .../OpenAICompatibleProviderCard.jsx | 54 ++- .../providers/VertexProviderCard.jsx | 87 +++- .../components/sections/AIContextSection.jsx | 103 ++-- options/components/sections/AboutSection.jsx | 4 +- .../components/sections/GeneralSection.jsx | 2 +- .../components/sections/ProvidersSection.jsx | 48 +- .../sections/TranslationSection.jsx | 217 +++++++-- options/hooks/useBackgroundReady.js | 54 ++- options/hooks/useDeepLTest.js | 213 +++++--- options/hooks/useOpenAITest.js | 153 +++--- options/hooks/useVertexTest.js | 457 ++++++++++-------- options/options.css | 19 +- options/options.html | 2 +- popup/PopupApp.jsx | 99 ++-- popup/components/SliderSetting.jsx | 19 +- popup/hooks/useSettings.js | 10 +- popup/hooks/useTranslation.js | 43 +- .../geminiVertexTranslate.js | 117 +++-- utils/vertexAuth.js | 30 +- vite.config.js | 11 +- 41 files changed, 1469 insertions(+), 768 deletions(-) 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 ( ); } - 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('providerNotes', 'Notes:')}
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:')}
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 }) { 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 }) { 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({ )) ) : ( - + )} @@ -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 ( - + {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 + ) } > @@ -132,31 +138,34 @@ export function AIContextSection({ t, settings, onSettingChange }) { - onSettingChange('aiContextTimeout', parseInt(e.target.value)) + onSettingChange( + 'aiContextTimeout', + parseInt(e.target.value) + ) } />
- 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 + ) } />
- 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( @@ -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( @@ -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( @@ -195,7 +216,10 @@ export function TranslationSection({ t, settings, onSettingChange }) {
{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('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 }) {
- {t('googleBatchSizeHelp', 'Recommended: 3-5 segments (default: 4)')} + {t( + 'googleBatchSizeHelp', + 'Recommended: 3-5 segments (default: 4)' + )}
- onSettingChange('googleBatchSize', parseInt(e.target.value)) + onSettingChange( + 'googleBatchSize', + parseInt(e.target.value) + ) } />
@@ -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 }) {
- {t('microsoftBatchSizeHelp', 'Recommended: 3-5 segments (default: 4)')} + {t( + 'microsoftBatchSizeHelp', + 'Recommended: 3-5 segments (default: 4)' + )}
- onSettingChange('microsoftBatchSize', parseInt(e.target.value)) + onSettingChange( + 'microsoftBatchSize', + parseInt(e.target.value) + ) } />
@@ -319,10 +382,16 @@ export function TranslationSection({ t, settings, onSettingChange }) {
- {t('vertexBatchSizeHelp', 'Recommended: 5-10 segments (default: 8)')} + {t( + 'vertexBatchSizeHelp', + 'Recommended: 5-10 segments (default: 8)' + )}
- onSettingChange('vertexBatchSize', parseInt(e.target.value)) + onSettingChange( + 'vertexBatchSize', + parseInt(e.target.value) + ) } />
@@ -341,7 +413,10 @@ export function TranslationSection({ t, settings, onSettingChange }) { )}
- {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 }) {
- {t('googleDelayHelp', 'Required delay to prevent temporary lockouts (default: 1500ms)')} + {t( + 'googleDelayHelp', + 'Required delay to prevent temporary lockouts (default: 1500ms)' + )}
- onSettingChange('googleDelay', parseInt(e.target.value)) + onSettingChange( + 'googleDelay', + parseInt(e.target.value) + ) } />
@@ -394,10 +487,16 @@ export function TranslationSection({ t, settings, onSettingChange }) {
- {t('deeplDelayHelp', 'Delay for DeepL API requests (default: 500ms)')} + {t( + 'deeplDelayHelp', + 'Delay for DeepL API requests (default: 500ms)' + )}
- onSettingChange('deeplDelay', parseInt(e.target.value)) + onSettingChange( + 'deeplDelay', + parseInt(e.target.value) + ) } />
@@ -416,10 +518,16 @@ export function TranslationSection({ t, settings, onSettingChange }) {
- {t('deeplFreeDelayHelp', 'Conservative delay for free tier (default: 2000ms)')} + {t( + 'deeplFreeDelayHelp', + 'Conservative delay for free tier (default: 2000ms)' + )}
- onSettingChange('deeplFreeDelay', parseInt(e.target.value)) + onSettingChange( + 'deeplFreeDelay', + parseInt(e.target.value) + ) } />
@@ -438,10 +549,16 @@ export function TranslationSection({ t, settings, onSettingChange }) {
- {t('microsoftDelayHelp', 'Delay to respect character limits (default: 800ms)')} + {t( + 'microsoftDelayHelp', + 'Delay to respect character limits (default: 800ms)' + )}
- onSettingChange('microsoftDelay', parseInt(e.target.value)) + onSettingChange( + 'microsoftDelay', + parseInt(e.target.value) + ) } />
@@ -460,10 +580,16 @@ export function TranslationSection({ t, settings, onSettingChange }) {
- {t('vertexDelayHelp', 'Minimum delay between requests (default: 100ms)')} + {t( + 'vertexDelayHelp', + 'Minimum delay between requests (default: 100ms)' + )}
- onSettingChange('vertexDelay', parseInt(e.target.value)) + onSettingChange( + 'vertexDelay', + parseInt(e.target.value) + ) } />
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 @@
- \ No newline at end of file + diff --git a/popup/PopupApp.jsx b/popup/PopupApp.jsx index ddaa3c8..6124ab8 100644 --- a/popup/PopupApp.jsx +++ b/popup/PopupApp.jsx @@ -1,5 +1,10 @@ import React, { useState, useEffect, useRef } from 'react'; -import { useSettings, useTranslation, useChromeMessage, useLogger } from './hooks/index.js'; +import { + useSettings, + useTranslation, + useChromeMessage, + useLogger, +} from './hooks/index.js'; import { Header } from './components/Header.jsx'; import { SettingToggle } from './components/SettingToggle.jsx'; import { LanguageSelector } from './components/LanguageSelector.jsx'; @@ -8,10 +13,12 @@ import { StatusMessage } from './components/StatusMessage.jsx'; export function PopupApp() { const { settings, updateSetting, loading, error } = useSettings(); - const { t, loading: translationsLoading } = useTranslation(settings.uiLanguage || 'en'); + const { t, loading: translationsLoading } = useTranslation( + settings.uiLanguage || 'en' + ); const { sendImmediateConfigUpdate } = useChromeMessage(); const logger = useLogger('Popup'); - + const [statusMessage, setStatusMessage] = useState(''); const statusTimeoutRef = useRef(null); @@ -19,7 +26,7 @@ export function PopupApp() { if (statusTimeoutRef.current) { clearTimeout(statusTimeoutRef.current); } - + setStatusMessage(message); statusTimeoutRef.current = setTimeout(() => { setStatusMessage(''); @@ -41,14 +48,19 @@ export function PopupApp() { logger.error('Error loading settings', error, { component: 'loadSettings', }); - showStatus('Failed to load settings. Please try refreshing the popup.', 5000); + showStatus( + 'Failed to load settings. Please try refreshing the popup.', + 5000 + ); } }, [error, logger]); const handleToggleSubtitles = async (enabled) => { try { await updateSetting('subtitlesEnabled', enabled); - const statusKey = enabled ? 'statusDualEnabled' : 'statusDualDisabled'; + const statusKey = enabled + ? 'statusDualEnabled' + : 'statusDualDisabled'; const statusText = t( statusKey, enabled ? 'Dual subtitles enabled.' : 'Dual subtitles disabled.' @@ -70,7 +82,7 @@ export function PopupApp() { try { await updateSetting('useNativeSubtitles', useOfficial); await updateSetting('useOfficialTranslations', useOfficial); - + const statusKey = useOfficial ? 'statusSmartTranslationEnabled' : 'statusSmartTranslationDisabled'; @@ -81,7 +93,7 @@ export function PopupApp() { : 'Official subtitles disabled.' ); showStatus(statusText); - + sendImmediateConfigUpdate({ useNativeSubtitles: useOfficial, useOfficialTranslations: useOfficial, @@ -93,7 +105,9 @@ export function PopupApp() { component: 'useNativeSubtitlesToggle', }); } - showStatus('Failed to update official subtitles setting. Please try again.'); + showStatus( + 'Failed to update official subtitles setting. Please try again.' + ); } }; @@ -134,7 +148,9 @@ export function PopupApp() { const handleLayoutOrderChange = async (layoutOrder) => { try { await updateSetting('subtitleLayoutOrder', layoutOrder); - showStatus(t('statusDisplayOrderUpdated', 'Display order updated.')); + showStatus( + t('statusDisplayOrderUpdated', 'Display order updated.') + ); sendImmediateConfigUpdate({ subtitleLayoutOrder: layoutOrder }); } catch (error) { if (logger) { @@ -150,8 +166,15 @@ export function PopupApp() { const handleLayoutOrientationChange = async (layoutOrientation) => { try { await updateSetting('subtitleLayoutOrientation', layoutOrientation); - showStatus(t('statusLayoutOrientationUpdated', 'Layout orientation updated.')); - sendImmediateConfigUpdate({ subtitleLayoutOrientation: layoutOrientation }); + showStatus( + t( + 'statusLayoutOrientationUpdated', + 'Layout orientation updated.' + ) + ); + sendImmediateConfigUpdate({ + subtitleLayoutOrientation: layoutOrientation, + }); } catch (error) { if (logger) { logger.error('Error setting layout orientation', error, { @@ -159,7 +182,9 @@ export function PopupApp() { component: 'subtitleLayoutOrientationSelect', }); } - showStatus('Failed to update layout orientation. Please try again.'); + showStatus( + 'Failed to update layout orientation. Please try again.' + ); } }; @@ -171,7 +196,9 @@ export function PopupApp() { const handleFontSizeChangeEnd = async (fontSize) => { try { await updateSetting('subtitleFontSize', fontSize); - showStatus(`${t('statusFontSize', 'Font size: ')}${fontSize.toFixed(1)}vw.`); + showStatus( + `${t('statusFontSize', 'Font size: ')}${fontSize.toFixed(1)}vw.` + ); sendImmediateConfigUpdate({ subtitleFontSize: fontSize }); } catch (error) { if (logger) { @@ -192,7 +219,9 @@ export function PopupApp() { const handleGapChangeEnd = async (gap) => { try { await updateSetting('subtitleGap', gap); - showStatus(`${t('statusVerticalGap', 'Vertical gap: ')}${gap.toFixed(1)}em.`); + showStatus( + `${t('statusVerticalGap', 'Vertical gap: ')}${gap.toFixed(1)}em.` + ); sendImmediateConfigUpdate({ subtitleGap: gap }); } catch (error) { if (logger) { @@ -207,20 +236,30 @@ export function PopupApp() { const handleVerticalPositionChange = (verticalPosition) => { // Real-time update without saving - sendImmediateConfigUpdate({ subtitleVerticalPosition: verticalPosition }); + sendImmediateConfigUpdate({ + subtitleVerticalPosition: verticalPosition, + }); }; const handleVerticalPositionChangeEnd = async (verticalPosition) => { try { await updateSetting('subtitleVerticalPosition', verticalPosition); - showStatus(`${t('statusVerticalPosition', 'Vertical position: ')}${verticalPosition.toFixed(1)}.`); - sendImmediateConfigUpdate({ subtitleVerticalPosition: verticalPosition }); + showStatus( + `${t('statusVerticalPosition', 'Vertical position: ')}${verticalPosition.toFixed(1)}.` + ); + sendImmediateConfigUpdate({ + subtitleVerticalPosition: verticalPosition, + }); } catch (error) { if (logger) { - logger.error('Error setting subtitle vertical position', error, { - verticalPosition, - component: 'subtitleVerticalPositionInput', - }); + logger.error( + 'Error setting subtitle vertical position', + error, + { + verticalPosition, + component: 'subtitleVerticalPositionInput', + } + ); } showStatus('Failed to update vertical position. Please try again.'); } @@ -230,7 +269,9 @@ export function PopupApp() { try { let offset = parseFloat(value); if (isNaN(offset)) { - showStatus(t('statusInvalidOffset', 'Invalid offset, reverting.')); + showStatus( + t('statusInvalidOffset', 'Invalid offset, reverting.') + ); return; } offset = parseFloat(offset.toFixed(2)); @@ -288,9 +329,10 @@ export function PopupApp() { } = settings; // Use useOfficialTranslations if available, fallback to useNativeSubtitles - const useOfficial = useOfficialTranslations !== undefined - ? useOfficialTranslations - : useNativeSubtitles; + const useOfficial = + useOfficialTranslations !== undefined + ? useOfficialTranslations + : useNativeSubtitles; return ( <> @@ -309,7 +351,10 @@ export function PopupApp() { diff --git a/popup/components/SliderSetting.jsx b/popup/components/SliderSetting.jsx index badf65c..8cdc6c8 100644 --- a/popup/components/SliderSetting.jsx +++ b/popup/components/SliderSetting.jsx @@ -12,12 +12,15 @@ export function SliderSetting({ }) { const sliderRef = useRef(null); - const updateSliderProgress = useCallback((sliderElement, val) => { - const minVal = parseFloat(min) || 0; - const maxVal = parseFloat(max) || 100; - const percentage = ((val - minVal) / (maxVal - minVal)) * 100; - sliderElement.style.backgroundSize = `${percentage}% 100%`; - }, [min, max]); + const updateSliderProgress = useCallback( + (sliderElement, val) => { + const minVal = parseFloat(min) || 0; + const maxVal = parseFloat(max) || 100; + const percentage = ((val - minVal) / (maxVal - minVal)) * 100; + sliderElement.style.backgroundSize = `${percentage}% 100%`; + }, + [min, max] + ); useEffect(() => { if (sliderRef.current) { @@ -48,7 +51,9 @@ export function SliderSetting({ step={step} value={value} onInput={handleInput} - onChange={(e) => onChangeEnd && onChangeEnd(parseFloat(e.target.value))} + onChange={(e) => + onChangeEnd && onChangeEnd(parseFloat(e.target.value)) + } /> {formatValue(value)}
diff --git a/popup/hooks/useSettings.js b/popup/hooks/useSettings.js index 004df12..dbad0ef 100644 --- a/popup/hooks/useSettings.js +++ b/popup/hooks/useSettings.js @@ -17,7 +17,7 @@ export function useSettings(keys) { try { setLoading(true); let data; - + if (Array.isArray(keys)) { data = await configService.getMultiple(keys); } else if (keys) { @@ -26,7 +26,7 @@ export function useSettings(keys) { } else { data = await configService.getAll(); } - + setSettings(data); setError(null); } catch (err) { @@ -43,7 +43,7 @@ export function useSettings(keys) { // Listen for setting changes useEffect(() => { const handleChange = (changes) => { - setSettings(prev => ({ ...prev, ...changes })); + setSettings((prev) => ({ ...prev, ...changes })); }; const unsubscribe = configService.onChanged(handleChange); @@ -60,7 +60,7 @@ export function useSettings(keys) { const updateSetting = useCallback(async (key, value) => { try { await configService.set(key, value); - setSettings(prev => ({ ...prev, [key]: value })); + setSettings((prev) => ({ ...prev, [key]: value })); return true; } catch (err) { setError(err); @@ -73,7 +73,7 @@ export function useSettings(keys) { const updateSettings = useCallback(async (updates) => { try { await configService.setMultiple(updates); - setSettings(prev => ({ ...prev, ...updates })); + setSettings((prev) => ({ ...prev, ...updates })); return true; } catch (err) { setError(err); diff --git a/popup/hooks/useTranslation.js b/popup/hooks/useTranslation.js index 2940c83..5c412d1 100644 --- a/popup/hooks/useTranslation.js +++ b/popup/hooks/useTranslation.js @@ -14,7 +14,7 @@ export function useTranslation(locale) { useEffect(() => { const loadTranslations = async () => { const normalizedLangCode = locale.replace('-', '_'); - + // Check cache first if (translationsCache[normalizedLangCode]) { setTranslations(translationsCache[normalizedLangCode]); @@ -29,11 +29,11 @@ export function useTranslation(locale) { try { setLoading(true); const response = await fetch(translationsPath); - + if (!response.ok) { throw new Error(`HTTP ${response.status}`); } - + const data = await response.json(); translationsCache[normalizedLangCode] = data; setTranslations(data); @@ -42,7 +42,7 @@ export function useTranslation(locale) { `Could not load '${normalizedLangCode}' translations, falling back to English`, error ); - + // Fallback to English try { const fallbackPath = chrome.runtime.getURL( @@ -70,22 +70,25 @@ export function useTranslation(locale) { }, [locale]); // Translation function - const t = useCallback((key, fallback = '', ...substitutions) => { - let message = translations[key]?.message || fallback || key; - - // Replace %s and %d placeholders with substitutions - if (substitutions.length > 0) { - let substitutionIndex = 0; - message = message.replace(/%[sd]/g, (match) => { - if (substitutionIndex < substitutions.length) { - return substitutions[substitutionIndex++]; - } - return match; - }); - } - - return message; - }, [translations]); + const t = useCallback( + (key, fallback = '', ...substitutions) => { + let message = translations[key]?.message || fallback || key; + + // Replace %s and %d placeholders with substitutions + if (substitutions.length > 0) { + let substitutionIndex = 0; + message = message.replace(/%[sd]/g, (match) => { + if (substitutionIndex < substitutions.length) { + return substitutions[substitutionIndex++]; + } + return match; + }); + } + + return message; + }, + [translations] + ); return { t, loading, translations }; } diff --git a/translation_providers/geminiVertexTranslate.js b/translation_providers/geminiVertexTranslate.js index 74e5871..405630d 100644 --- a/translation_providers/geminiVertexTranslate.js +++ b/translation_providers/geminiVertexTranslate.js @@ -48,20 +48,23 @@ function parsePossiblyJson(responseText) { if (typeof responseText !== 'string' || responseText.trim() === '') { return ''; } - const jsonMatch = responseText.match(/```json\s*([\s\S]*?)\s*```|(\[[\s\S]*\])/); - const jsonString = jsonMatch ? (jsonMatch[1] || jsonMatch[2]) : null; + const jsonMatch = responseText.match( + /```json\s*([\s\S]*?)\s*```|(\[[\s\S]*\])/ + ); + const jsonString = jsonMatch ? jsonMatch[1] || jsonMatch[2] : null; if (!jsonString) { return responseText; } try { return JSON.parse(jsonString); } catch (e) { - logger.warn('Response looked like JSON but failed to parse, using raw text.'); + logger.warn( + 'Response looked like JSON but failed to parse, using raw text.' + ); return responseText; } } - // Ensure model name is in short form (e.g., "gemini-1.5-flash"), removing any leading path like // "models/gemini-1.5-flash" or "publishers/google/models/gemini-1.5-flash". function normalizeModelName(model) { @@ -100,7 +103,9 @@ export async function translate(text, sourceLang, targetLang) { const { accessToken, projectId, location, model } = await getConfig(); if (!accessToken || !projectId || !location || !model) { - throw new Error('Vertex access token, project, location, or model not configured.'); + throw new Error( + 'Vertex access token, project, location, or model not configured.' + ); } const endpoint = buildVertexEndpoint( @@ -125,10 +130,18 @@ export async function translate(text, sourceLang, targetLang) { const requestBody = { contents: [ - { role: 'user', parts: [{ text: `${systemPrompt}\n\n${userPrompt}\n\n${text}` }] }, + { + role: 'user', + parts: [ + { text: `${systemPrompt}\n\n${userPrompt}\n\n${text}` }, + ], + }, ], generationConfig: { - maxOutputTokens: Math.max(256, Math.min(2048, Math.ceil(text.length * 3))), + maxOutputTokens: Math.max( + 256, + Math.min(2048, Math.ceil(text.length * 3)) + ), }, }; @@ -150,34 +163,31 @@ export async function translate(text, sourceLang, targetLang) { errorMessage = parsed.error.message; } } catch (e) {} - logger.error( - 'Vertex AI single translation failed', - null, - { - status: response.status, - statusText: response.statusText, - endpoint, - errorMessage, - } + logger.error('Vertex AI single translation failed', null, { + status: response.status, + statusText: response.statusText, + endpoint, + errorMessage, + }); + throw new Error( + `Vertex translation error: ${response.status} ${response.statusText}` ); - throw new Error(`Vertex translation error: ${response.status} ${response.statusText}`); } const data = await response.json(); - const responseText = data?.candidates?.[0]?.content?.parts?.[0]?.text || ''; + const responseText = + data?.candidates?.[0]?.content?.parts?.[0]?.text || ''; if (!responseText) { throw new Error('Empty response from Vertex AI'); } - return typeof responseText === 'string' ? responseText.trim() : String(responseText); + return typeof responseText === 'string' + ? responseText.trim() + : String(responseText); } catch (error) { - logger.error( - 'Fatal error during Vertex AI single translation', - error, - { - sourceLang, - targetLang, - } - ); + logger.error('Fatal error during Vertex AI single translation', error, { + sourceLang, + targetLang, + }); return text; // Fallback to original } } @@ -211,7 +221,9 @@ export async function translateBatch( const { accessToken, projectId, location, model } = await getConfig(); if (!accessToken || !projectId || !location || !model) { - throw new Error('Vertex access token, project, location, or model not configured.'); + throw new Error( + 'Vertex access token, project, location, or model not configured.' + ); } const endpoint = buildVertexEndpoint( @@ -235,10 +247,16 @@ Important: const requestBody = { contents: [ - { role: 'user', parts: [{ text: `${instructions}\n\n${combinedText}` }] }, + { + role: 'user', + parts: [{ text: `${instructions}\n\n${combinedText}` }], + }, ], generationConfig: { - maxOutputTokens: Math.min(4096, Math.max(500, combinedText.length * 3)), + maxOutputTokens: Math.min( + 4096, + Math.max(500, combinedText.length * 3) + ), }, }; @@ -260,21 +278,20 @@ Important: errorMessage = parsed.error.message; } } catch (e) {} - logger.error( - 'Vertex AI batch translation failed', - null, - { - status: response.status, - statusText: response.statusText, - endpoint, - errorMessage, - } + logger.error('Vertex AI batch translation failed', null, { + status: response.status, + statusText: response.statusText, + endpoint, + errorMessage, + }); + throw new Error( + `Vertex batch translation error: ${response.status}` ); - throw new Error(`Vertex batch translation error: ${response.status}`); } const data = await response.json(); - const responseText = data?.candidates?.[0]?.content?.parts?.[0]?.text || ''; + const responseText = + data?.candidates?.[0]?.content?.parts?.[0]?.text || ''; if (!responseText) { throw new Error('Empty response from Vertex AI'); } @@ -284,7 +301,9 @@ Important: if (Array.isArray(parsed)) { // Ensure array length matches input size if (parsed.length !== texts.length) { - throw new Error('Translated array length does not match input array length.'); + throw new Error( + 'Translated array length does not match input array length.' + ); } return parsed.map((s) => (typeof s === 'string' ? s : String(s))); } @@ -301,15 +320,11 @@ Important: } return split.map((s) => s.trim()); } catch (error) { - logger.error( - 'Fatal error during Vertex AI batch translation', - error, - { - sourceLang, - targetLang, - textCount: texts.length, - } - ); + logger.error('Fatal error during Vertex AI batch translation', error, { + sourceLang, + targetLang, + textCount: texts.length, + }); return texts; // Fallback to original array } } diff --git a/utils/vertexAuth.js b/utils/vertexAuth.js index d986f97..b6c630e 100644 --- a/utils/vertexAuth.js +++ b/utils/vertexAuth.js @@ -81,7 +81,8 @@ export async function getAccessTokenFromServiceAccount(serviceAccountJson) { const now = Math.floor(Date.now() / 1000); const iat = now; const exp = now + 3600; // 1 hour - const tokenUri = serviceAccountJson.token_uri || 'https://oauth2.googleapis.com/token'; + const tokenUri = + serviceAccountJson.token_uri || 'https://oauth2.googleapis.com/token'; const scope = 'https://www.googleapis.com/auth/cloud-platform'; const header = { alg: 'RS256', typ: 'JWT' }; @@ -93,7 +94,11 @@ export async function getAccessTokenFromServiceAccount(serviceAccountJson) { exp, }; - const jwt = await signJwtRS256(header, claims, serviceAccountJson.private_key); + const jwt = await signJwtRS256( + header, + claims, + serviceAccountJson.private_key + ); const body = new URLSearchParams(); body.set('grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer'); @@ -107,14 +112,19 @@ export async function getAccessTokenFromServiceAccount(serviceAccountJson) { if (!res.ok) { const text = await res.text(); - throw new Error(`Token exchange failed: ${res.status} ${res.statusText} ${text}`); + throw new Error( + `Token exchange failed: ${res.status} ${res.statusText} ${text}` + ); } const data = await res.json(); if (!data.access_token) { throw new Error('Token exchange response missing access_token'); } - return { accessToken: data.access_token, expiresIn: data.expires_in || 3600 }; + return { + accessToken: data.access_token, + expiresIn: data.expires_in || 3600, + }; } /** @@ -170,10 +180,11 @@ export async function refreshAccessToken(updateConfig = true) { } // Generate new token - const { accessToken, expiresIn } = await getAccessTokenFromServiceAccount(sa); + const { accessToken, expiresIn } = + await getAccessTokenFromServiceAccount(sa); // Calculate new expiration time - const expiresAt = Date.now() + (expiresIn * 1000); + const expiresAt = Date.now() + expiresIn * 1000; // Update expiration time in storage await chrome.storage.local.set({ @@ -183,7 +194,9 @@ export async function refreshAccessToken(updateConfig = true) { // Update config if requested if (updateConfig && typeof chrome.storage.sync !== 'undefined') { try { - const { configService } = await import('../services/configService.js'); + const { configService } = await import( + '../services/configService.js' + ); await configService.set('vertexAccessToken', accessToken); } catch (error) { console.error('[VertexAuth] Failed to update config:', error); @@ -199,7 +212,7 @@ export async function refreshAccessToken(updateConfig = true) { */ export async function autoRefreshIfNeeded() { const expirationInfo = await checkTokenExpiration(); - + if (!expirationInfo) { return null; } @@ -217,4 +230,3 @@ export async function autoRefreshIfNeeded() { return null; } - diff --git a/vite.config.js b/vite.config.js index 86460b0..ecfde65 100644 --- a/vite.config.js +++ b/vite.config.js @@ -11,10 +11,10 @@ export default defineConfig({ closeBundle() { const distDir = 'dist'; mkdirSync(distDir, { recursive: true }); - + // Copy manifest.json copyFileSync('manifest.json', `${distDir}/manifest.json`); - + // Copy all extension files that aren't built by Vite const filesToCopy = [ 'background.js', @@ -30,12 +30,15 @@ export default defineConfig({ 'translation_providers', 'context_providers', ]; - + filesToCopy.forEach((file) => { try { cpSync(file, `${distDir}/${file}`, { recursive: true }); } catch (err) { - console.warn(`Warning: Could not copy ${file}:`, err.message); + console.warn( + `Warning: Could not copy ${file}:`, + err.message + ); } }); },