Skip to content

Commit d89a742

Browse files
committed
Add Google translator API key support
1 parent 900bdf1 commit d89a742

File tree

5 files changed

+139
-14
lines changed

5 files changed

+139
-14
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
## [Unreleased]
66

7+
### Added
8+
9+
- Allow configuring a Google Cloud Translation API key for the Google translator, including secure credential storage and Compose UI controls.
10+
711
## [4.1.0] - 2025-10-11
812

913
### Added

src/main/kotlin/com/airsaid/localization/translate/impl/google/AbsGoogleTranslator.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ abstract class AbsGoogleTranslator : AbstractTranslator() {
3131

3232
override val icon: Icon = PluginIcons.GOOGLE_ICON
3333

34-
override val credentialDefinitions: List<TranslatorCredentialDescriptor> = emptyList()
34+
override val credentialDefinitions: List<TranslatorCredentialDescriptor> = listOf(API_KEY_DESCRIPTOR)
3535

3636
override val supportedLanguages: List<Lang> by lazy {
3737
Languages.allSupportedLanguages()
@@ -46,4 +46,14 @@ abstract class AbsGoogleTranslator : AbstractTranslator() {
4646
}
4747
}
4848
}
49+
50+
companion object {
51+
val API_KEY_DESCRIPTOR = TranslatorCredentialDescriptor(
52+
id = "apiKey",
53+
label = "API Key",
54+
isSecret = true,
55+
required = false,
56+
description = "Optional Google Cloud Translation API key"
57+
)
58+
}
4959
}

src/main/kotlin/com/airsaid/localization/translate/impl/google/GoogleTranslator.kt

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,21 @@ class GoogleTranslator : AbsGoogleTranslator() {
2424

2525
private val log = Logger.getInstance(GoogleTranslator::class.java)
2626

27+
private val useCustomApiKey: Boolean
28+
get() = GoogleTranslatorSettings.getInstance().useCustomApiKey
29+
2730
override val key: String = KEY
2831

2932
override val icon: Icon = PluginIcons.GOOGLE_ICON
3033

3134
override fun getRequestUrl(fromLang: Lang, toLang: Lang, text: String): String {
35+
if (useCustomApiKey) {
36+
val apiKey = credentialValue(API_KEY_DESCRIPTOR.id)
37+
return UrlBuilder(CLOUD_TRANSLATE_V2_URL)
38+
.addQueryParameter("key", apiKey)
39+
.build()
40+
}
41+
3242
val source = if (fromLang.code.equals(Languages.AUTO.code, ignoreCase = true)) "auto" else fromLang.translationCode
3343
val builder = UrlBuilder(googleApiUrl(TRANSLATE_PATH))
3444
.addQueryParameter("client", "gtx")
@@ -44,17 +54,51 @@ class GoogleTranslator : AbsGoogleTranslator() {
4454
}
4555

4656
override fun getRequestParams(fromLang: Lang, toLang: Lang, text: String): List<Pair<String, String>> {
57+
if (useCustomApiKey) return emptyList()
58+
4759
return listOf(Pair.create("q", text))
4860
}
4961

5062
override fun configureRequestBuilder(requestBuilder: RequestBuilder) {
51-
requestBuilder.withGoogleHeaders()
63+
if (!useCustomApiKey) {
64+
requestBuilder.withGoogleHeaders()
65+
}
66+
}
67+
68+
override val requestContentType: String
69+
get() = if (useCustomApiKey) JSON_CONTENT_TYPE else super.requestContentType
70+
71+
override fun getRequestBody(fromLang: Lang, toLang: Lang, text: String): String {
72+
if (!useCustomApiKey) return ""
73+
74+
val request = GoogleCloudTranslationRequest(
75+
q = listOf(text),
76+
target = toLang.translationCode,
77+
format = "text",
78+
source = fromLang.takeUnless { it.code.equals(Languages.AUTO.code, ignoreCase = true) }?.translationCode
79+
)
80+
return GsonUtil.getInstance().gson.toJson(request)
5281
}
5382

5483
/**
5584
* Parses the JSON payload and surfaces API errors as `TranslationException`.
5685
*/
5786
override fun parsingResult(fromLang: Lang, toLang: Lang, text: String, resultText: String): String {
87+
return if (useCustomApiKey) {
88+
parseCloudResponse(fromLang, toLang, text, resultText)
89+
} else {
90+
parseWebResponse(fromLang, toLang, text, resultText)
91+
}
92+
}
93+
94+
companion object {
95+
private const val KEY = "Google"
96+
private const val TRANSLATE_PATH = "/translate_a/single"
97+
private const val CLOUD_TRANSLATE_V2_URL = "https://translation.googleapis.com/language/translate/v2"
98+
private const val JSON_CONTENT_TYPE = "application/json; charset=UTF-8"
99+
}
100+
101+
private fun parseWebResponse(fromLang: Lang, toLang: Lang, text: String, resultText: String): String {
58102
val response = GsonUtil.getInstance().gson.fromJson(resultText, GoogleTranslationResponse::class.java)
59103
response.error?.message?.let { message ->
60104
throw TranslationException(fromLang, toLang, text, message)
@@ -73,8 +117,36 @@ class GoogleTranslator : AbsGoogleTranslator() {
73117
return translation
74118
}
75119

76-
companion object {
77-
private const val KEY = "Google"
78-
private const val TRANSLATE_PATH = "/translate_a/single"
120+
private fun parseCloudResponse(fromLang: Lang, toLang: Lang, text: String, resultText: String): String {
121+
val response = GsonUtil.getInstance().gson.fromJson(resultText, GoogleCloudTranslationResponse::class.java)
122+
response.error?.message?.let { message ->
123+
throw TranslationException(fromLang, toLang, text, message)
124+
}
125+
val translation = response.data?.translations
126+
?.mapNotNull { it.translatedText }
127+
?.joinToString(separator = "")
128+
?.trim()
129+
.orEmpty()
130+
if (translation.isEmpty()) {
131+
log.warn("Empty translation from Google Cloud Translation API: $resultText")
132+
return ""
133+
}
134+
return translation
79135
}
80136
}
137+
138+
private data class GoogleCloudTranslationRequest(
139+
val q: List<String>,
140+
val target: String,
141+
val format: String,
142+
val source: String?
143+
)
144+
145+
private data class GoogleCloudTranslationResponse(
146+
val data: TranslationData?,
147+
val error: CloudError?
148+
) {
149+
data class TranslationData(val translations: List<TranslationEntry>?)
150+
data class TranslationEntry(val translatedText: String?)
151+
data class CloudError(val message: String?)
152+
}

src/main/kotlin/com/airsaid/localization/translate/impl/google/GoogleTranslatorSettings.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ class GoogleTranslatorSettings : PersistentStateComponent<GoogleTranslatorSettin
1717

1818
data class State(
1919
var useCustomServer: Boolean = false,
20-
var serverUrl: String = DEFAULT_SERVER_URL
20+
var serverUrl: String = DEFAULT_SERVER_URL,
21+
var useCustomApiKey: Boolean = false,
2122
)
2223

2324
private var state = State()
@@ -34,6 +35,12 @@ class GoogleTranslatorSettings : PersistentStateComponent<GoogleTranslatorSettin
3435
state = state.copy(serverUrl = value.ifBlank { DEFAULT_SERVER_URL })
3536
}
3637

38+
var useCustomApiKey: Boolean
39+
get() = state.useCustomApiKey
40+
set(value) {
41+
state = state.copy(useCustomApiKey = value)
42+
}
43+
3744
override fun getState(): State = state
3845

3946
override fun loadState(state: State) {

src/main/kotlin/com/airsaid/localization/translate/impl/google/GoogleTranslatorSettingsDialog.kt

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import androidx.compose.foundation.layout.Arrangement
44
import androidx.compose.foundation.layout.Column
55
import androidx.compose.foundation.layout.fillMaxWidth
66
import androidx.compose.foundation.layout.padding
7-
import androidx.compose.foundation.text.selection.SelectionContainer
87
import androidx.compose.runtime.*
98
import androidx.compose.ui.Modifier
109
import androidx.compose.ui.unit.dp
10+
import com.airsaid.localization.config.SettingsState
1111
import com.airsaid.localization.ui.ComposeDialog
1212
import com.airsaid.localization.ui.components.IdeCheckBox
1313
import com.airsaid.localization.ui.components.IdeTextField
@@ -22,9 +22,11 @@ import org.jetbrains.jewel.ui.component.Text
2222
class GoogleTranslatorSettingsDialog : ComposeDialog() {
2323

2424
override val defaultPreferredSize
25-
get() = 400 to 160
25+
get() = 500 to 260
2626

2727
private val settings = GoogleTranslatorSettings.getInstance()
28+
private val state = SettingsState.getInstance()
29+
private val apiKeyDescriptor = AbsGoogleTranslator.API_KEY_DESCRIPTOR
2830

2931
init {
3032
title = "Google Translator Settings"
@@ -34,6 +36,8 @@ class GoogleTranslatorSettingsDialog : ComposeDialog() {
3436
override fun Content() {
3537
var useCustomServer by remember { mutableStateOf(settings.useCustomServer) }
3638
var serverUrl by remember { mutableStateOf(settings.serverUrl) }
39+
var useCustomApiKey by remember { mutableStateOf(settings.useCustomApiKey) }
40+
var apiKey by remember { mutableStateOf(state.getCredential("Google", apiKeyDescriptor)) }
3741

3842
Column(
3943
modifier = Modifier
@@ -44,11 +48,12 @@ class GoogleTranslatorSettingsDialog : ComposeDialog() {
4448
checked = useCustomServer,
4549
onCheckedChange = {
4650
useCustomServer = it
47-
if (!it) {
48-
serverUrl = settings.serverUrl
51+
if (useCustomServer) {
52+
useCustomApiKey = false
4953
}
5054
},
5155
title = "Use custom server",
56+
subTitle = "Defaults to translate.googleapis.com unless a custom server is used."
5257
)
5358

5459
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
@@ -71,20 +76,47 @@ class GoogleTranslatorSettingsDialog : ComposeDialog() {
7176
)
7277
}
7378

74-
SelectionContainer {
79+
IdeCheckBox(
80+
checked = useCustomApiKey,
81+
onCheckedChange = {
82+
useCustomApiKey = it
83+
if (useCustomApiKey) {
84+
useCustomServer = false
85+
}
86+
},
87+
title = "Use custom API key",
88+
subTitle = "Disable to fall back to the public web endpoint."
89+
)
90+
91+
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
7592
Text(
76-
text = "Defaults to translate.googleapis.com when not specified.",
93+
text = "API Key",
7794
color = JewelTheme.globalColors.text.info
7895
)
96+
IdeTextField(
97+
value = apiKey,
98+
onValueChange = { apiKey = it },
99+
modifier = Modifier.fillMaxWidth(),
100+
enabled = useCustomApiKey,
101+
secureInput = true,
102+
placeholder = {
103+
Text(
104+
text = "Enter your Google Cloud Translation API key",
105+
color = JewelTheme.globalColors.text.info
106+
)
107+
}
108+
)
79109
}
80110
}
81111

82112
OnClickOK {
83-
// Persist the selected endpoint and toggle when the user accepts the dialog.
84113
settings.useCustomServer = useCustomServer
85114
if (useCustomServer) {
86115
settings.serverUrl = serverUrl.ifBlank { GoogleTranslatorSettings.DEFAULT_SERVER_URL }
87116
}
117+
settings.useCustomApiKey = useCustomApiKey
118+
val normalizedKey = if (useCustomApiKey) apiKey.trim() else ""
119+
state.setCredential("Google", apiKeyDescriptor, normalizedKey)
88120
}
89121
}
90-
}
122+
}

0 commit comments

Comments
 (0)