From becefa7fe0d985432ea6b87f492ec57023bcb772 Mon Sep 17 00:00:00 2001 From: otoneko1102 Date: Sun, 22 Mar 2026 20:10:34 +0900 Subject: [PATCH 1/2] =?UTF-8?q?Google=E7=BF=BB=E8=A8=B3API=E5=AF=BE?= =?UTF-8?q?=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 43 +++++++++++++++----- plugin.js | 114 +++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 136 insertions(+), 21 deletions(-) diff --git a/index.html b/index.html index ce7a25c..bc347b0 100644 --- a/index.html +++ b/index.html @@ -584,7 +584,7 @@
翻訳エンジン設定
-
+
+ +
@@ -957,9 +972,10 @@ const res = await fetch(`${API_BASE}?type=settings`) const json = await res.json() const s = json.response || json - document.getElementById('apiKey').value = s.apiKeySet ? '(設定済み)' : '' - document.getElementById('engineSelect').value = s.engine || 'deepl' - document.getElementById('targetLang').value = s.targetLang || 'JA' + document.getElementById('apiKey').value = s.apiKeySet ? '(設定済み)' : '' + document.getElementById('googleApiKey').value = s.googleApiKeySet ? '(設定済み)' : '' + document.getElementById('engineSelect').value = s.engine || 'deepl' + document.getElementById('targetLang').value = s.targetLang || 'JA' document.getElementById('ollamaModelText').value = s.ollamaModel || 'translategemma:4b' onEngineChange(s.engine || 'deepl') } catch (e) { @@ -968,15 +984,16 @@ } function onEngineChange(engine) { - document.getElementById('ollamaModelRow').style.display = - engine === 'ollama' ? '' : 'none' - document.getElementById('apiKey').closest('.form-row').style.display = - engine === 'ollama' ? 'none' : '' + document.getElementById('deeplKeyRow').style.display = engine === 'deepl' ? '' : 'none' + document.getElementById('googleKeyRow').style.display = engine === 'google' ? '' : 'none' + document.getElementById('ollamaModelRow').style.display = engine === 'ollama' ? '' : 'none' } async function saveSettings() { - const apiKeyEl = document.getElementById('apiKey') - const apiKeyVal = apiKeyEl.value.trim() + const apiKeyEl = document.getElementById('apiKey') + const googleApiKeyEl = document.getElementById('googleApiKey') + const apiKeyVal = apiKeyEl.value.trim() + const googleApiKeyVal = googleApiKeyEl.value.trim() const body = { engine: document.getElementById('engineSelect').value, @@ -987,6 +1004,9 @@ if (apiKeyVal && apiKeyVal !== '(設定済み)') { body.apiKey = apiKeyVal } + if (googleApiKeyVal && googleApiKeyVal !== '(設定済み)') { + body.googleApiKey = googleApiKeyVal + } try { const res = await fetch(API_BASE, { @@ -1002,6 +1022,9 @@ if (body.apiKey) { apiKeyEl.value = '(設定済み)' } + if (body.googleApiKey) { + googleApiKeyEl.value = '(設定済み)' + } } else { result.textContent = '❌ 保存に失敗しました' result.style.color = 'var(--error)' diff --git a/plugin.js b/plugin.js index f4e00e6..d5bb4f9 100644 --- a/plugin.js +++ b/plugin.js @@ -4,9 +4,11 @@ const https = require('https') const http = require('http') const PLUGIN_UID = 'com.qua121.comment-translator' -const DEEPL_FREE_API_HOST = 'api-free.deepl.com' -const DEEPL_PRO_API_HOST = 'api.deepl.com' -const DEEPL_API_PATH = '/v2/translate' +const DEEPL_FREE_API_HOST = 'api-free.deepl.com' +const DEEPL_PRO_API_HOST = 'api.deepl.com' +const DEEPL_API_PATH = '/v2/translate' +const GOOGLE_TRANSLATE_HOST = 'translation.googleapis.com' +const GOOGLE_TRANSLATE_PATH = '/language/translate/v2' const OLLAMA_HOST = 'localhost' const OLLAMA_PORT = 11434 const OLLAMA_PATH = '/api/chat' @@ -18,6 +20,17 @@ const REQUEST_TIMEOUT_MS = 30000 const OLLAMA_TIMEOUT_MS = 300000 // 通常翻訳: 5分 const OLLAMA_FIRST_TIMEOUT_MS = 3600000 // 初回モデルロード: 1時間 +const GOOGLE_LANG_MAP = { + 'JA': 'ja', + 'EN-US': 'en', + 'EN-GB': 'en', + 'ZH': 'zh', + 'KO': 'ko', + 'FR': 'fr', + 'DE': 'de', + 'ES': 'es', +} + const LANG_NAME_FOR_PROMPT = { 'JA': 'Japanese', 'EN-US': 'English', @@ -136,6 +149,50 @@ function isDeepLFreeKey(apiKey) { return apiKey.endsWith(':fx') } +function callGoogleTranslateAPI(text, apiKey, targetLang = 'JA') { + return new Promise((resolve, reject) => { + const target = GOOGLE_LANG_MAP[targetLang] || targetLang.toLowerCase().split('-')[0] + const body = JSON.stringify({ q: text, target, format: 'text' }) + + const options = { + hostname: GOOGLE_TRANSLATE_HOST, + path: `${GOOGLE_TRANSLATE_PATH}?key=${encodeURIComponent(apiKey)}`, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(body), + }, + } + + const req = https.request(options, (res) => { + let data = '' + res.on('data', (chunk) => { data += chunk }) + res.on('end', () => { + if (res.statusCode === 200) { + try { + const json = JSON.parse(data) + resolve(json.data.translations[0].translatedText) + } catch (e) { + reject({ code: 'PARSE_ERROR', message: e.message }) + } + } else { + reject({ code: `GOOGLE_${res.statusCode}`, message: data }) + } + }) + }) + + req.on('error', (e) => reject({ code: 'GOOGLE_NETWORK_ERROR', message: e.message })) + + req.setTimeout(REQUEST_TIMEOUT_MS, () => { + req.destroy() + reject({ code: 'TIMEOUT', message: 'Request timed out' }) + }) + + req.write(body) + req.end() + }) +} + function callDeepLAPI(text, apiKey, targetLang = 'JA') { return new Promise((resolve, reject) => { const body = new URLSearchParams({ text, target_lang: targetLang }).toString() @@ -335,6 +392,26 @@ const ERROR_CATALOG = { solution: 'デバッグログを確認してください', link: null, }, + GOOGLE_403: { + cause: 'Google APIキーが無効、またはCloud Translation APIが有効化されていません', + solution: 'Google Cloud ConsoleでAPIキーとCloud Translation APIの設定を確認してください', + link: 'https://console.cloud.google.com/apis/library/translate.googleapis.com', + }, + GOOGLE_400: { + cause: 'Google翻訳APIへのリクエストが不正です', + solution: 'デバッグログを確認してください', + link: null, + }, + GOOGLE_429: { + cause: 'Google翻訳APIのレート制限に達しました', + solution: 'しばらく待つと自動回復します', + link: null, + }, + GOOGLE_NETWORK_ERROR: { + cause: 'Google翻訳サーバーに接続できません', + solution: 'インターネット接続を確認してください', + link: null, + }, } function categorizeError(code) { @@ -355,6 +432,7 @@ const plugin = { defaultState: { apiKey: '', + googleApiKey: '', engine: 'deepl', targetLang: 'JA', ollamaModel: 'translategemma:4b', @@ -383,8 +461,11 @@ const plugin = { this._log('INFO', `plugin dir: ${dir}`) this._log('INFO', `engine: ${store.get('engine')} / targetLang: ${store.get('targetLang')}`) - if (store.get('engine') !== 'ollama' && !store.get('apiKey')) { + const engine = store.get('engine') + if (engine === 'deepl' && !store.get('apiKey')) { this._log('INFO', 'DeepL APIキーが未設定です。設定ページから入力してください。') + } else if (engine === 'google' && !store.get('googleApiKey')) { + this._log('INFO', 'Google APIキーが未設定です。設定ページから入力してください。') } }, @@ -447,12 +528,13 @@ const plugin = { const engine = store.get('engine') || 'deepl' const targetLang = store.get('targetLang') || 'JA' - if (engine === 'deepl') { - const apiKey = store.get('apiKey') - if (!apiKey) { - this._log('DEBUG', `翻訳スキップ: APIキー未設定 (text="${text.slice(0, 20)}")`) - return - } + if (engine === 'deepl' && !store.get('apiKey')) { + this._log('DEBUG', `翻訳スキップ: DeepL APIキー未設定 (text="${text.slice(0, 20)}")`) + return + } + if (engine === 'google' && !store.get('googleApiKey')) { + this._log('DEBUG', `翻訳スキップ: Google APIキー未設定 (text="${text.slice(0, 20)}")`) + return } const cached = this._translationCache?.get(text) @@ -496,6 +578,12 @@ const plugin = { throw retryErr } } + } else if (engine === 'google') { + const googleApiKey = store.get('googleApiKey') + this._log('INFO', `google translate: "${text.slice(0, 20)}"`) + translated = await this._queue.add(() => + callGoogleTranslateAPI(text, googleApiKey, targetLang) + ) } else { const apiKey = store.get('apiKey') translated = await this._queue.add(() => @@ -614,6 +702,7 @@ const plugin = { code: 200, response: { apiKeySet: !!store.get('apiKey'), + googleApiKeySet: !!store.get('googleApiKey'), engine: store.get('engine'), targetLang: store.get('targetLang'), ollamaModel: store.get('ollamaModel') || 'translategemma:4b', @@ -644,7 +733,10 @@ const plugin = { store.set('apiKey', body.apiKey) this._commentStructureLogged = false } - if (body.engine !== undefined && (body.engine === 'deepl' || body.engine === 'ollama')) { + if (body.googleApiKey !== undefined) { + store.set('googleApiKey', body.googleApiKey) + } + if (body.engine !== undefined && (body.engine === 'deepl' || body.engine === 'ollama' || body.engine === 'google')) { store.set('engine', body.engine) if (this._translationCache) this._translationCache.clear() } From cc6a2825a5cae2b2ede6e4c1b8e3117def401520 Mon Sep 17 00:00:00 2001 From: otoneko1102 Date: Sun, 22 Mar 2026 20:14:25 +0900 Subject: [PATCH 2/2] =?UTF-8?q?Google=E7=BF=BB=E8=A8=B3API=E3=81=B8?= =?UTF-8?q?=E3=81=AE=E5=AF=BE=E5=BF=9C=E6=96=B9=E6=B3=95=E3=81=AA=E3=81=A9?= =?UTF-8?q?=E3=82=92=E8=A8=98=E8=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 31 +++++++++++++++++++++++++------ package.json | 5 +++-- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 0fa1127..63121e3 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ - わんコメ v5.2 以上 - 翻訳エンジン(いずれか): - **DeepL**(推奨): インターネット接続 + DeepL アカウント(無料) + - **Google翻訳**: インターネット接続 + Google Cloud アカウント - **Ollama**: NVIDIA GPU推奨(VRAM 6GB以上)、インターネット不要 ## 導入手順 @@ -39,7 +40,15 @@ #### DeepL を使う場合 1. **設定** タブを開く -2. DeepL APIキーを入力して **保存** +2. 翻訳エンジンを **DeepL** に選択 +3. DeepL APIキーを入力して **保存** + +#### Google翻訳 を使う場合 + +1. [Google Cloud Console](https://console.cloud.google.com/apis/library/translate.googleapis.com) で **Cloud Translation API** を有効化 +2. APIキーを発行する(認証情報 → APIキーを作成) +3. **設定** タブで翻訳エンジンを **Google翻訳** に選択 +4. Google Cloud APIキーを入力して **保存** #### Ollama(ローカルLLM)を使う場合 @@ -74,18 +83,22 @@ ### データの外部送信について -このプラグインは翻訳のため、**コメントのテキストをDeepL社のサーバーに送信します**。 +選択した翻訳エンジンに応じて、**コメントのテキストが外部サーバーに送信されます**。 -- DeepL Free APIでは、送信されたテキストがDeepLのサービス改善に使用される場合があります -- DeepL Pro APIでは、翻訳後すぐに削除されます -- 詳細は [DeepL利用規約](https://www.deepl.com/terms) をご確認ください +| エンジン | 送信先 | 備考 | +|----------|--------|------| +| DeepL | DeepL社サーバー | Free APIはサービス改善に使用される場合あり | +| Google翻訳 | Google社サーバー | [Google Cloud プライバシーポリシー](https://cloud.google.com/terms/cloud-privacy-notice) 参照 | +| Ollama | ローカルのみ | 外部送信なし | -ユーザー自身がAPIキーを発行・設定することで、この送信に同意したものとみなします。 +ユーザー自身がAPIキーを発行・設定することで、各サービスへの送信に同意したものとみなします。 ### 文字数制限について DeepL Free APIは月50万文字まで無料です。通常の配信コメント量では超過しませんが、超過した場合はエラータブに通知が表示されます。 +Google Cloud Translation API は一定量(月500,000文字)の無料枠があります。超過後は従量課金となります。詳細は [Google Cloud 料金ページ](https://cloud.google.com/translate/pricing) を確認してください。 + --- ## 免責事項 @@ -108,6 +121,12 @@ DeepL Free APIは月50万文字まで無料です。通常の配信コメント - DeepL Free API では、送信されたテキストがサービス改善に利用される場合があります - DeepL の利用規約([https://www.deepl.com/terms](https://www.deepl.com/terms))への準拠はユーザーの責任です +#### Google翻訳 を使用する場合 + +- コメントのテキストが Google 社のサーバーに送信されます +- Cloud Translation API の利用規約([https://cloud.google.com/terms](https://cloud.google.com/terms))への準拠はユーザーの責任です +- 無料枠超過後は従量課金が発生します。利用量の管理はユーザーの責任です + #### Ollama(ローカルLLM)を使用する場合 - 使用するモデルの選択はユーザー自身が行います diff --git a/package.json b/package.json index 1a3a634..8062aa0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "com.qua121.comment-translator", "version": "0.3.1", - "description": "わんコメ用コメント翻訳プラグイン(DeepL / Ollama対応)", + "description": "わんコメ用コメント翻訳プラグイン(DeepL / Google翻訳 / Ollama対応)", "private": true, "main": "plugin.js", "author": "qua121", @@ -11,6 +11,7 @@ "plugin", "translation", "deepl", + "google-translate", "ollama" ] -} +} \ No newline at end of file