Skip to content
Closed
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## [Unreleased]

- Added a proxy setting for translation requests.
- 新增翻译请求的代理设置

## [3.9.1] (2026/08/26)

- Fixed the issue of cache conflicts in document translation for Google Translate.
Expand Down
67 changes: 67 additions & 0 deletions src/main/kotlin/cn/yiiguxing/plugin/translate/Settings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import com.intellij.util.xmlb.XmlSerializerUtil
import com.intellij.util.xmlb.annotations.Tag
import com.intellij.util.xmlb.annotations.Transient
import org.jetbrains.concurrency.runAsync
import java.net.InetSocketAddress
import java.net.Proxy
import kotlin.properties.Delegates

/**
Expand Down Expand Up @@ -186,6 +188,11 @@ class Settings : PersistentStateComponent<Settings> {
*/
var translationWindowLocation: WindowLocation = WindowLocation.MOUSE_SCREEN

/**
* 网络代理
*/
var proxySettings: ProxySettings = ProxySettings()

override fun getState(): Settings = this

override fun loadState(state: Settings) {
Expand Down Expand Up @@ -308,6 +315,66 @@ class BaiduTranslateSettings : AppKeySettings(BAIDU_APP_KEY)
*/
class AliTranslateSettings : AppKeySettings(ALI_APP_KEY)

/**
* 代理类型
*/
enum class ProxyType(val displayName: String) {
HTTP(message("settings.proxy.type.http")),
SOCKS(message("settings.proxy.type.socks"))
}

/**
* 网络代理选项
*/
@Tag("proxy")
class ProxySettings {

/**
* 是否启用代理
*/
var enabled: Boolean = false

/**
* 代理类型
*/
var type: ProxyType = ProxyType.HTTP

/**
* 代理主机。
*
* 只填主机名或 IP 地址,**不要带 `http://` 之类的协议前缀**:该值会直接传给
* [InetSocketAddress],带前缀时整个字符串会被当作主机名去解析,得到未解析地址,
* 最终在连接阶段报出与代理配置看不出关联的错误。代理协议由 [type] 决定。
*/
var host: String = ""

/**
* 代理端口
*/
var port: Int = 0

/**
* 返回用于创建连接的代理,未启用或配置不完整时返回 `null`。
*
* [host] 与 [port] 直接用于构造 [InetSocketAddress],因此 [host] 必须是裸主机名或 IP。
*/
fun toProxyOrNull(): Proxy? {
if (!enabled || host.isBlank() || port !in MIN_PORT..MAX_PORT) {
return null
}
val javaType = when (type) {
ProxyType.HTTP -> Proxy.Type.HTTP
ProxyType.SOCKS -> Proxy.Type.SOCKS
}
return Proxy(javaType, InetSocketAddress(host, port))
}

companion object {
private const val MIN_PORT = 1
private const val MAX_PORT = 65535
}
}

enum class TTSSource(val displayName: String) {
ORIGINAL(message("settings.item.original")),
TRANSLATION(message("settings.item.translation"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import cn.yiiguxing.plugin.translate.util.md5
import cn.yiiguxing.plugin.translate.util.splitSentence
import cn.yiiguxing.plugin.translate.util.type
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.util.io.HttpRequests
import kotlinx.coroutines.*
import kotlin.text.any
import kotlin.time.Duration.Companion.milliseconds
Expand Down Expand Up @@ -68,7 +67,7 @@ internal class BingTranslator(
object : AsyncExpiringData<BingAuthentication>(scope) {
override suspend fun load(): BingAuthentication {
val html = withContext(Dispatchers.IO) {
HttpRequests.request(BING_TRANSLATOR_URL).setUserAgent().readString()
Http.newRequest(BING_TRANSLATOR_URL).setUserAgent().readString()
}
return parseBingAuthentication(html)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import com.intellij.ide.plugins.marketplace.setHeadersViaTuner
import com.intellij.openapi.diagnostic.Attachment
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.diagnostic.logger
import com.intellij.util.io.HttpRequests
import com.intellij.util.io.RequestBuilder
import java.lang.reflect.Type

Expand Down Expand Up @@ -151,7 +150,7 @@ internal object MicrosoftHttp {
builder: RequestBuilder.() -> Unit
): String {
return try {
HttpRequests.post(url, contentType)
Http.newRequest(url, contentType)
.accept(Http.MIME_TYPE_JSON)
.apply(builder)
.send(data) { it.readString() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import com.google.gson.JsonParseException
import com.google.gson.annotations.SerializedName
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.diagnostic.logger
import com.intellij.util.io.HttpRequests
import com.intellij.util.io.RequestBuilder

internal object OpenAiHttp {
Expand All @@ -27,7 +26,7 @@ internal object OpenAiHttp {
fun <T> post(url: String, block: RequestBuilder.() -> T): T {
return try {
block(
HttpRequests.post(url, Http.MIME_TYPE_JSON)
Http.newRequest(url, Http.MIME_TYPE_JSON)
.accept(Http.MIME_TYPE_JSON)
)
} catch (e: Http.StatusException) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import cn.yiiguxing.plugin.translate.util.Observable
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.progress.EmptyProgressIndicator
import com.intellij.openapi.project.Project
import com.intellij.util.io.HttpRequests
import java.io.IOException
import java.util.*

Expand Down Expand Up @@ -66,7 +65,7 @@ class GoogleTTSPlayer private constructor(

override fun onLoad(): ByteArray {
val url = getTtsUrl(sentences[index++], lang)
val response: TextToSpeechResponse = HttpRequests.request(url)
val response: TextToSpeechResponse = Http.newRequest(url)
.setUserAgent()
.connect {
Http.defaultGson.fromJson(it.getReader(indicator), TextToSpeechResponse::class.java)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package cn.yiiguxing.plugin.translate.tts.microsoft

import cn.yiiguxing.plugin.translate.Settings
import cn.yiiguxing.plugin.translate.trans.Lang
import cn.yiiguxing.plugin.translate.tts.sound.source.PushablePlaybackSource
import cn.yiiguxing.plugin.translate.util.Http
Expand Down Expand Up @@ -50,6 +51,7 @@ internal class EdgeTTSSource(
private val settings: EdgeTTSSettings by lazy { EdgeTTSSettings.instance() }

init {
Settings.getInstance().proxySettings.toProxyOrNull()?.let { setProxy(it) }
addHeader("Pragma", "no-cache")
addHeader("Cache-Control", "no-cache")
addHeader("Accept-Encoding", "gzip, deflate, br")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package cn.yiiguxing.plugin.translate.ui.settings

import cn.yiiguxing.plugin.translate.ProxyType
import cn.yiiguxing.plugin.translate.Settings
import cn.yiiguxing.plugin.translate.TTSSource
import cn.yiiguxing.plugin.translate.TranslationStates
Expand Down Expand Up @@ -241,8 +242,14 @@ internal class SettingsPanel(
|| settings.showActionsInContextMenuOnlyWithSelection != showActionsInContextMenuOnlyWithSelectionCheckbox.isSelected
|| states.maxHistorySize != maxHistoriesSizeComboBox.item
|| settings.translationWindowLocation != translationWindowLocationComboBox.selected
|| settings.proxySettings.enabled != proxyCheckBox.isSelected
|| settings.proxySettings.type != proxyTypeComboBox.selected
|| settings.proxySettings.host != proxyHostField.text.trim()
|| settings.proxySettings.port != proxyPort
}

private val proxyPort: Int get() = proxyPortField.text.trim().toIntOrNull() ?: 0

private fun getConfigurationPath(vararg configurations: String): String = configurations.joinToString("|") {
it.trim(' ', '\n', ':', ':')
}
Expand Down Expand Up @@ -301,6 +308,29 @@ internal class SettingsPanel(
ignoreRegex = this@SettingsPanel.ignoreRegExp.text
translationWindowLocation = translationWindowLocationComboBox.selected ?: WindowLocation.MOUSE_SCREEN
}

applyProxySettings()
}

private fun applyProxySettings() {
val enabled = proxyCheckBox.isSelected
val host = proxyHostField.text.trim()
val port = proxyPort
if (enabled && (host.isEmpty() || port !in 1..65535)) {
throw ConfigurationException(
message(
"settings.invalid.configuration",
getConfigurationPath(message("settings.panel.title.proxy"))
)
)
}

with(settings.proxySettings) {
this.enabled = enabled
type = proxyTypeComboBox.selected ?: ProxyType.HTTP
this.host = host
this.port = port
}
}

private fun throwConfigurationException(name: String): Nothing {
Expand Down Expand Up @@ -338,6 +368,13 @@ internal class SettingsPanel(
takeWordCheckBox.isSelected = settings.takeWordWhenDialogOpens
wordbookStoragePathField.text = settings.wordbookStoragePath ?: ""
translationWindowLocationComboBox.selected = settings.translationWindowLocation

with(settings.proxySettings) {
proxyCheckBox.isSelected = enabled
proxyTypeComboBox.selected = type
proxyHostField.text = host
proxyPortField.text = port.takeIf { it > 0 }?.toString().orEmpty()
}
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package cn.yiiguxing.plugin.translate.ui.settings

import cn.yiiguxing.plugin.translate.ProxyType
import cn.yiiguxing.plugin.translate.TTSSource
import cn.yiiguxing.plugin.translate.TranslationStorages
import cn.yiiguxing.plugin.translate.message
Expand Down Expand Up @@ -201,6 +202,33 @@ internal abstract class SettingsUi {
renderer = textListCellRenderer { it.displayName }
}

protected val proxyCheckBox: JBCheckBox = JBCheckBox(message("settings.proxy.enabled")).apply {
addItemListener { fixProxyComponentsEnabled() }
}

protected val proxyTypeComboBox: ComboBox<ProxyType> = comboBox<ProxyType>().apply {
renderer = textListCellRenderer { it.displayName }
}

/**
* 代理主机。使用 [JBTextField] 的占位提示来展示期望的格式:这里只接受裸主机名或 IP,
* 带 `http://` 前缀会让 [java.net.InetSocketAddress] 解析失败。
*/
protected val proxyHostField: JBTextField = JBTextField().apply {
emptyText.text = "127.0.0.1"
}

protected val proxyPortField: JTextField = JTextField().apply {
document = object : PlainDocument() {
override fun insertString(offset: Int, str: String?, attr: AttributeSet?) {
val digits = str?.filter(Char::isDigit)?.take(5) ?: return
if (digits.isNotEmpty()) {
super.insertString(offset, digits, attr)
}
}
}
}

protected val supportLinkLabel: LinkLabel<*> =
LinkLabel<Any>(message("support.or.donate"), TranslationIcons.Support).apply {
border = JBUI.Borders.emptyTop(20)
Expand Down Expand Up @@ -235,6 +263,30 @@ internal abstract class SettingsUi {
add(JLabel(message("settings.label.targetLanguage")))
add(targetLanguageComboBox, wrap().sizeGroupX(comboboxGroup))
}
val proxyPanel = titledPanel(message("settings.panel.title.proxy")) {
add(proxyCheckBox, wrap().span(2))

add(JLabel(message("settings.label.proxy.type")))
add(proxyTypeComboBox, wrap())

add(JLabel(message("settings.label.proxy.host")))
add(proxyHostField, fillX().wrap())
setMinWidth(proxyHostField, JBUIScale.scale(250))

val hostComment = ComponentPanelBuilder.createCommentComponent(
message("settings.comment.proxy.host"), true
)
val hostCommentCC = fillX()
.gapBefore(JBUIScale.scale(2).toString())
.gapTop(JBUIScale.scale(2).toString())
.span(2)
.wrap()
add(hostComment, hostCommentCC)

add(JLabel(message("settings.label.proxy.port")))
add(proxyPortField, wrap())
}

val textSelectionPanel = titledPanel(message("settings.panel.title.text.selection"), true) {
add(keepFormatCheckBox, wrap().span(4))
add(takeNearestWordCheckBox, wrap().span(4))
Expand Down Expand Up @@ -355,6 +407,7 @@ internal abstract class SettingsUi {

wholePanel.addVertically(
generalPanel,
proxyPanel,
fontsPanel,
textSelectionPanel,
translationPopupPanel,
Expand All @@ -365,13 +418,22 @@ internal abstract class SettingsUi {
otherPanel,
supportLinkLabel
)

fixProxyComponentsEnabled()
}

fun createMainPanel(): JPanel {
doLayout()
return wholePanel
}

private fun fixProxyComponentsEnabled() {
val enabled = proxyCheckBox.isSelected
proxyTypeComboBox.isEnabled = enabled
proxyHostField.isEnabled = enabled
proxyPortField.isEnabled = enabled
}

private fun fixEngineConfigurationComponent() {
configureTranslationEngineButton.isVisible = translationEngineComboBox.selected?.hasConfiguration ?: false
}
Expand Down
Loading