Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 25 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- わんコメ v5.2 以上
- 翻訳エンジン(いずれか):
- **DeepL**(推奨): インターネット接続 + DeepL アカウント(無料)
- **Google翻訳**: インターネット接続 + Google Cloud アカウント
- **Ollama**: NVIDIA GPU推奨(VRAM 6GB以上)、インターネット不要

## 導入手順
Expand Down Expand Up @@ -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)を使う場合

Expand Down Expand Up @@ -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) を確認してください。

---

## 免責事項
Expand All @@ -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)を使用する場合

- 使用するモデルの選択はユーザー自身が行います
Expand Down
43 changes: 33 additions & 10 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@
<div class="settings-section">
<div class="settings-title">翻訳エンジン設定</div>

<div class="form-row">
<div class="form-row" id="deeplKeyRow">
<label for="apiKey">DeepL APIキー(Free / Pro 自動判定)</label>
<input
type="password"
Expand All @@ -599,10 +599,25 @@
</span>
</div>

<div class="form-row" id="googleKeyRow" style="display:none">
<label for="googleApiKey">Google Cloud APIキー</label>
<input
type="password"
id="googleApiKey"
placeholder="AIzaSy..."
autocomplete="off"
/>
<span style="font-size:11px; color:var(--text-dim)">
<a href="https://console.cloud.google.com/apis/library/translate.googleapis.com" target="_blank" style="color:var(--accent)">Google Cloud Console</a> でCloud Translation APIを有効化し、APIキーを発行してください。
※ コメントテキストがGoogleサーバーに送信されます
</span>
</div>

<div class="form-row">
<label for="engineSelect">翻訳エンジン</label>
<select id="engineSelect" onchange="onEngineChange(this.value)">
<option value="deepl">DeepL(推奨)</option>
<option value="google">Google翻訳</option>
<option value="ollama">ローカル / Ollama</option>
</select>
</div>
Expand Down Expand Up @@ -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) {
Expand All @@ -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,
Expand All @@ -987,6 +1004,9 @@
if (apiKeyVal && apiKeyVal !== '(設定済み)') {
body.apiKey = apiKeyVal
}
if (googleApiKeyVal && googleApiKeyVal !== '(設定済み)') {
body.googleApiKey = googleApiKeyVal
}

try {
const res = await fetch(API_BASE, {
Expand All @@ -1002,6 +1022,9 @@
if (body.apiKey) {
apiKeyEl.value = '(設定済み)'
}
if (body.googleApiKey) {
googleApiKeyEl.value = '(設定済み)'
}
} else {
result.textContent = '❌ 保存に失敗しました'
result.style.color = 'var(--error)'
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -11,6 +11,7 @@
"plugin",
"translation",
"deepl",
"google-translate",
"ollama"
]
}
}
114 changes: 103 additions & 11 deletions plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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',
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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) {
Expand All @@ -355,6 +432,7 @@ const plugin = {

defaultState: {
apiKey: '',
googleApiKey: '',
engine: 'deepl',
targetLang: 'JA',
ollamaModel: 'translategemma:4b',
Expand Down Expand Up @@ -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キーが未設定です。設定ページから入力してください。')
}
},

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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(() =>
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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()
}
Expand Down