diff --git a/backend/api/src/main/kotlin/io/tolgee/hateoas/machineTranslation/SuggestResultModel.kt b/backend/api/src/main/kotlin/io/tolgee/hateoas/machineTranslation/SuggestResultModel.kt index 9aff999e76..ac212ca443 100644 --- a/backend/api/src/main/kotlin/io/tolgee/hateoas/machineTranslation/SuggestResultModel.kt +++ b/backend/api/src/main/kotlin/io/tolgee/hateoas/machineTranslation/SuggestResultModel.kt @@ -30,7 +30,11 @@ class SuggestResultModel( "TOLGEE": { "output": "This was translated by Tolgee Translator", "contextDescription": "This is an example in swagger" - } + }, + "OPENAI": { + "output": "This was translated by OpenAI", + "contextDescription": null + } } """, ) diff --git a/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/translationSuggestionController/TranslationSuggestionControllerMtTest.kt b/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/translationSuggestionController/TranslationSuggestionControllerMtTest.kt index 3727e4c68b..892e3bbb9f 100644 --- a/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/translationSuggestionController/TranslationSuggestionControllerMtTest.kt +++ b/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/translationSuggestionController/TranslationSuggestionControllerMtTest.kt @@ -250,6 +250,7 @@ class TranslationSuggestionControllerMtTest : ProjectAuthControllerTest("/v2/pro node("AZURE").isEqualTo("Translated with Azure Cognitive") node("BAIDU").isEqualTo("Translated with Baidu") node("TOLGEE").isEqualTo("Translated with Tolgee Translator") + node("OPENAI").isEqualTo("Translated with OpenAI") } mtCreditBucketService.getCreditBalances( @@ -275,6 +276,7 @@ class TranslationSuggestionControllerMtTest : ProjectAuthControllerTest("/v2/pro node("AZURE").isAbsent() node("BAIDU").isAbsent() node("TOLGEE").isEqualTo("Translated with Tolgee Translator") + node("OPENAI").isEqualTo("Translated with OpenAI") } } } diff --git a/backend/data/src/main/kotlin/io/tolgee/component/machineTranslation/providers/OpenaiApiService.kt b/backend/data/src/main/kotlin/io/tolgee/component/machineTranslation/providers/OpenaiApiService.kt new file mode 100644 index 0000000000..7cec85a484 --- /dev/null +++ b/backend/data/src/main/kotlin/io/tolgee/component/machineTranslation/providers/OpenaiApiService.kt @@ -0,0 +1,76 @@ +package io.tolgee.component.machineTranslation.providers + +import com.fasterxml.jackson.annotation.JsonProperty +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import io.tolgee.configuration.tolgee.machineTranslation.OpenaiMachineTranslationProperties +import org.springframework.beans.factory.config.ConfigurableBeanFactory +import org.springframework.context.annotation.Scope +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.MediaType +import org.springframework.stereotype.Component +import org.springframework.web.client.RestTemplate + +@Component +@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON) +class OpenaiApiService( + private val openaiMachineTranslationProperties: OpenaiMachineTranslationProperties, + private val restTemplate: RestTemplate, +) { + fun translate( + text: String, + sourceTag: String, + targetTag: String, + ): String? { + var headers = HttpHeaders() + headers.contentType = MediaType.APPLICATION_JSON + headers.add("Authorization", "Bearer ${openaiMachineTranslationProperties.apiKey}") + + var prompt = openaiMachineTranslationProperties.prompt + prompt = prompt.replace("{source}", sourceTag) + prompt = prompt.replace("{target}", targetTag) + prompt = prompt.replace("{text}", text) + + val requestBody = JsonObject() + requestBody.addProperty("model", openaiMachineTranslationProperties.model) + requestBody.add( + "messages", + JsonArray().apply { + add( + JsonObject().apply { + addProperty("role", "user") + addProperty("content", prompt) + }, + ) + }, + ) + + val response = + restTemplate.postForEntity( + openaiMachineTranslationProperties.apiEndpoint, + HttpEntity(requestBody.toString(), headers), + OpenaiCompletionResponse::class.java, + ) + + return response.body?.choices?.first()?.message?.content + ?: throw RuntimeException(response.toString()) + } + + companion object { + class OpenaiCompletionResponse { + @JsonProperty("choices") + var choices: List? = null + } + + class OpenaiChoice { + @JsonProperty("message") + var message: OpenaiMessage? = null + } + + class OpenaiMessage { + @JsonProperty("content") + var content: String? = null + } + } +} diff --git a/backend/data/src/main/kotlin/io/tolgee/component/machineTranslation/providers/OpenaiTranslationProvider.kt b/backend/data/src/main/kotlin/io/tolgee/component/machineTranslation/providers/OpenaiTranslationProvider.kt new file mode 100644 index 0000000000..ca74deff39 --- /dev/null +++ b/backend/data/src/main/kotlin/io/tolgee/component/machineTranslation/providers/OpenaiTranslationProvider.kt @@ -0,0 +1,126 @@ +package io.tolgee.component.machineTranslation.providers + +import io.tolgee.component.machineTranslation.MtValueProvider +import io.tolgee.configuration.tolgee.machineTranslation.OpenaiMachineTranslationProperties +import org.springframework.beans.factory.config.ConfigurableBeanFactory +import org.springframework.context.annotation.Scope +import org.springframework.stereotype.Component + +@Component +@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON) +class OpenaiTranslationProvider( + private val openaiMachineTranslationProperties: OpenaiMachineTranslationProperties, + private val openaiApiService: OpenaiApiService, +) : AbstractMtValueProvider() { + override val isEnabled: Boolean + get() = !openaiMachineTranslationProperties.apiKey.isNullOrEmpty() + + override fun translateViaProvider(params: ProviderTranslateParams): MtValueProvider.MtResult { + val result = + openaiApiService.translate( + params.text, + params.sourceLanguageTag, + params.targetLanguageTag, + ) + return MtValueProvider.MtResult(result, params.text.length * 100) + } + + override val formalitySupportingLanguages = + arrayOf( + "de", + "es", + "fr", + "it", + "ja", + "nl", + "pl", + "pt-pt", + "pt-br", + "ru", + ) + + override val supportedLanguages = + arrayOf( + "yue", + "yue-hans", + "yue-hans-cn", + "kor", + "ko", + "ko-kr", + "th", + "th-th", + "pt", + "pt-pt", + "pt-br", + "el", + "el-gr", + "bul", + "bg", + "bg-bg", + "fin", + "fi", + "fi-fi", + "slo", + "sk", + "sk-sk", + "cht", + "zh-hant", + "zh-hant-hk", + "zh-hant-mo", + "zh-hant-tw", + "zh-tw", + "zh", + "zh-hans", + "zh-hans-cn", + "zh-hans-sg", + "zh-hans-hk", + "zh-hans-mo", + "wyw", + "fra", + "fr", + "fr-fr", + "ara", + "ar", + "de", + "de-de", + "nl", + "nl", + "nl-nl", + "est", + "et", + "et-ee", + "cs", + "cs-cz", + "swe", + "sl", + "sl-si", + "sv", + "sv-se", + "vie", + "vi", + "vi-vn", + "en", + "en-us", + "en-gb", + "jp", + "ja", + "ja-jp", + "spa", + "es", + "es-es", + "ru", + "ru-ru", + "it", + "it-it", + "pl", + "pl-pl", + "dan", + "da", + "da-dk", + "rom", + "ro", + "ro-ro", + "hu", + "hu-hu", + ) +} diff --git a/backend/data/src/main/kotlin/io/tolgee/configuration/tolgee/machineTranslation/MachineTranslationProperties.kt b/backend/data/src/main/kotlin/io/tolgee/configuration/tolgee/machineTranslation/MachineTranslationProperties.kt index cbad629467..682110525f 100644 --- a/backend/data/src/main/kotlin/io/tolgee/configuration/tolgee/machineTranslation/MachineTranslationProperties.kt +++ b/backend/data/src/main/kotlin/io/tolgee/configuration/tolgee/machineTranslation/MachineTranslationProperties.kt @@ -15,6 +15,7 @@ open class MachineTranslationProperties( var azure: AzureCognitiveTranslationProperties = AzureCognitiveTranslationProperties(), var baidu: BaiduMachineTranslationProperties = BaiduMachineTranslationProperties(), var tolgee: TolgeeMachineTranslationProperties = TolgeeMachineTranslationProperties(), + var openai: OpenaiMachineTranslationProperties = OpenaiMachineTranslationProperties(), @DocProperty( description = "Amount of machine translations users of the Free tier can request per month. " + diff --git a/backend/data/src/main/kotlin/io/tolgee/configuration/tolgee/machineTranslation/OpenaiMachieTranslationProperties.kt b/backend/data/src/main/kotlin/io/tolgee/configuration/tolgee/machineTranslation/OpenaiMachieTranslationProperties.kt new file mode 100644 index 0000000000..6dd6f3ccbe --- /dev/null +++ b/backend/data/src/main/kotlin/io/tolgee/configuration/tolgee/machineTranslation/OpenaiMachieTranslationProperties.kt @@ -0,0 +1,28 @@ +package io.tolgee.configuration.tolgee.machineTranslation + +import io.tolgee.configuration.annotations.DocProperty +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties(prefix = "tolgee.machine-translation.openai") +@DocProperty( + description = "OpenAI machine translation properties", + displayName = "OpenAI Translate", +) +open class OpenaiMachineTranslationProperties( + @DocProperty(description = "Whether OpenAI-powered machine translation is enabled.") + override var defaultEnabled: Boolean = true, + @DocProperty(description = "Whether to use OpenAI Translate as a primary translation engine.") + override var defaultPrimary: Boolean = false, + @DocProperty(description = "OpenAI API key") + var apiKey: String? = null, + @DocProperty(description = "OpenAI model to use for translation") + var model: String = "gpt-4o-mini", + @DocProperty(description = "OpenAI API endpoint") + var apiEndpoint: String = "https://api.openai.com/v1/chat/completions", + @DocProperty( + description = "Translation prompt. Should contain {source}, {target} and {text} placeholders.", + ) + var prompt: String = + "Translate the following text from {source} to {target}: \"{text}\". " + + "Do not include any other information in the response.", +) : MachineTranslationServiceProperties diff --git a/backend/data/src/main/kotlin/io/tolgee/constants/MtServiceType.kt b/backend/data/src/main/kotlin/io/tolgee/constants/MtServiceType.kt index eadf62da20..5c647ad5b6 100644 --- a/backend/data/src/main/kotlin/io/tolgee/constants/MtServiceType.kt +++ b/backend/data/src/main/kotlin/io/tolgee/constants/MtServiceType.kt @@ -6,6 +6,7 @@ import io.tolgee.component.machineTranslation.providers.AzureCognitiveTranslatio import io.tolgee.component.machineTranslation.providers.BaiduTranslationProvider import io.tolgee.component.machineTranslation.providers.DeeplTranslationProvider import io.tolgee.component.machineTranslation.providers.GoogleTranslationProvider +import io.tolgee.component.machineTranslation.providers.OpenaiTranslationProvider import io.tolgee.component.machineTranslation.providers.tolgee.TolgeeTranslationProvider import io.tolgee.configuration.tolgee.machineTranslation.AwsMachineTranslationProperties import io.tolgee.configuration.tolgee.machineTranslation.AzureCognitiveTranslationProperties @@ -13,6 +14,7 @@ import io.tolgee.configuration.tolgee.machineTranslation.BaiduMachineTranslation import io.tolgee.configuration.tolgee.machineTranslation.DeeplMachineTranslationProperties import io.tolgee.configuration.tolgee.machineTranslation.GoogleMachineTranslationProperties import io.tolgee.configuration.tolgee.machineTranslation.MachineTranslationServiceProperties +import io.tolgee.configuration.tolgee.machineTranslation.OpenaiMachineTranslationProperties import io.tolgee.configuration.tolgee.machineTranslation.TolgeeMachineTranslationProperties enum class MtServiceType( @@ -54,4 +56,9 @@ enum class MtServiceType( order = -1, supportsPlurals = true, ), + OPENAI( + propertyClass = OpenaiMachineTranslationProperties::class.java, + providerClass = OpenaiTranslationProvider::class.java, + order = 6, + ), } diff --git a/backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/SuggestionTestData.kt b/backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/SuggestionTestData.kt index 591aaaec8f..cde535633d 100644 --- a/backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/SuggestionTestData.kt +++ b/backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/SuggestionTestData.kt @@ -172,6 +172,7 @@ class SuggestionTestData : BaseTestData() { MtServiceType.AZURE, MtServiceType.BAIDU, MtServiceType.TOLGEE, + MtServiceType.OPENAI, ) this.primaryService = MtServiceType.AWS } @@ -187,6 +188,7 @@ class SuggestionTestData : BaseTestData() { MtServiceType.DEEPL, MtServiceType.AZURE, MtServiceType.BAIDU, + MtServiceType.OPENAI, ) this.primaryService = MtServiceType.GOOGLE } diff --git a/backend/testing/src/main/kotlin/io/tolgee/AbstractSpringTest.kt b/backend/testing/src/main/kotlin/io/tolgee/AbstractSpringTest.kt index ddf894614d..7037c21beb 100644 --- a/backend/testing/src/main/kotlin/io/tolgee/AbstractSpringTest.kt +++ b/backend/testing/src/main/kotlin/io/tolgee/AbstractSpringTest.kt @@ -15,6 +15,7 @@ import io.tolgee.configuration.tolgee.machineTranslation.BaiduMachineTranslation import io.tolgee.configuration.tolgee.machineTranslation.DeeplMachineTranslationProperties import io.tolgee.configuration.tolgee.machineTranslation.GoogleMachineTranslationProperties import io.tolgee.configuration.tolgee.machineTranslation.MachineTranslationProperties +import io.tolgee.configuration.tolgee.machineTranslation.OpenaiMachineTranslationProperties import io.tolgee.configuration.tolgee.machineTranslation.TolgeeMachineTranslationProperties import io.tolgee.constants.MtServiceType import io.tolgee.development.DbPopulatorReal @@ -166,6 +167,9 @@ abstract class AbstractSpringTest : AbstractTransactionalTest() { @Autowired lateinit var tolgeeMachineTranslationProperties: TolgeeMachineTranslationProperties + @Autowired + lateinit var openaiMachineTranslationProperties: OpenaiMachineTranslationProperties + @Autowired lateinit var internalProperties: InternalProperties @@ -256,6 +260,7 @@ abstract class AbstractSpringTest : AbstractTransactionalTest() { tolgeeMachineTranslationProperties.url = "http://localhost:8081" tolgeeMachineTranslationProperties.defaultEnabled = enabledServices.contains(MtServiceType.TOLGEE) internalProperties.fakeMtProviders = false + openaiMachineTranslationProperties.apiKey = "dummy" } fun executeInNewTransaction(fn: (ts: TransactionStatus) -> T): T { diff --git a/webapp/public/images/providers/openai-icon-dark.svg b/webapp/public/images/providers/openai-icon-dark.svg new file mode 100644 index 0000000000..2bdd7fded2 --- /dev/null +++ b/webapp/public/images/providers/openai-icon-dark.svg @@ -0,0 +1,11 @@ + + + openai-icon-dark + + + + + + + + \ No newline at end of file diff --git a/webapp/public/images/providers/openai-icon-light.svg b/webapp/public/images/providers/openai-icon-light.svg new file mode 100644 index 0000000000..29ad15556b --- /dev/null +++ b/webapp/public/images/providers/openai-icon-light.svg @@ -0,0 +1,11 @@ + + + openai-icon-light + + + + + + + + \ No newline at end of file diff --git a/webapp/src/service/apiSchema.generated.ts b/webapp/src/service/apiSchema.generated.ts index 47d4400655..250e596ca5 100644 --- a/webapp/src/service/apiSchema.generated.ts +++ b/webapp/src/service/apiSchema.generated.ts @@ -2687,6 +2687,7 @@ export interface components { | "AZURE" | "BAIDU" | "TOLGEE" + | "OPENAI" )[]; /** @description Info about enabled services */ enabledServicesInfo: components["schemas"]["MtServiceInfo"][]; @@ -2700,7 +2701,8 @@ export interface components { | "DEEPL" | "AZURE" | "BAIDU" - | "TOLGEE"; + | "TOLGEE" + | "OPENAI"; primaryServiceInfo?: components["schemas"]["MtServiceInfo"]; /** * Format: int64 @@ -2832,6 +2834,7 @@ export interface components { | "AZURE" | "BAIDU" | "TOLGEE" + | "OPENAI" )[]; /** @description Info about enabled services */ enabledServicesInfo?: components["schemas"]["MtServiceInfo"][]; @@ -2845,7 +2848,8 @@ export interface components { | "DEEPL" | "AZURE" | "BAIDU" - | "TOLGEE"; + | "TOLGEE" + | "OPENAI"; primaryServiceInfo?: components["schemas"]["MtServiceInfo"]; /** * Format: int64 @@ -2887,7 +2891,7 @@ export interface components { /** @description Info about enabled services */ MtServiceInfo: { formality?: "FORMAL" | "INFORMAL" | "DEFAULT"; - serviceType: "GOOGLE" | "AWS" | "DEEPL" | "AZURE" | "BAIDU" | "TOLGEE"; + serviceType: "GOOGLE" | "AWS" | "DEEPL" | "AZURE" | "BAIDU" | "TOLGEE" | "OPENAI"; }; MtServicesDTO: { defaultPrimaryService?: @@ -2896,12 +2900,13 @@ export interface components { | "DEEPL" | "AZURE" | "BAIDU" - | "TOLGEE"; + | "TOLGEE" + | "OPENAI"; services: { [key: string]: components["schemas"]["MtServiceDTO"] }; }; MtSupportedService: { formalitySupported: boolean; - serviceType: "GOOGLE" | "AWS" | "DEEPL" | "AZURE" | "BAIDU" | "TOLGEE"; + serviceType: "GOOGLE" | "AWS" | "DEEPL" | "AZURE" | "BAIDU" | "TOLGEE" | "OPENAI"; }; NamespaceModel: { /** @@ -4335,7 +4340,7 @@ export interface components { keyId?: number; plural?: boolean; /** @description List of services to use. If null, then all enabled services are used. */ - services?: ("GOOGLE" | "AWS" | "DEEPL" | "AZURE" | "BAIDU" | "TOLGEE")[]; + services?: ("GOOGLE" | "AWS" | "DEEPL" | "AZURE" | "BAIDU" | "TOLGEE" | "OPENAI")[]; /** Format: int64 */ targetLanguageId: number; }; @@ -4534,7 +4539,7 @@ export interface components { */ id: number; /** @description Which machine translation service was used to auto translate this */ - mtProvider?: "GOOGLE" | "AWS" | "DEEPL" | "AZURE" | "BAIDU" | "TOLGEE"; + mtProvider?: "GOOGLE" | "AWS" | "DEEPL" | "AZURE" | "BAIDU" | "TOLGEE" | "OPENAI"; /** @description Whether base language translation was changed after this translation was updated */ outdated: boolean; /** @description State of translation */ @@ -4570,7 +4575,7 @@ export interface components { */ id: number; /** @description Which machine translation service was used to auto translate this */ - mtProvider?: "GOOGLE" | "AWS" | "DEEPL" | "AZURE" | "BAIDU" | "TOLGEE"; + mtProvider?: "GOOGLE" | "AWS" | "DEEPL" | "AZURE" | "BAIDU" | "TOLGEE" | "OPENAI"; /** @description Whether base language translation was changed after this translation was updated */ outdated: boolean; /** @description State of translation */ diff --git a/webapp/src/views/projects/languages/MachineTranslation/getServiceName.tsx b/webapp/src/views/projects/languages/MachineTranslation/getServiceName.tsx index 35113434a7..fe4c8e6fe7 100644 --- a/webapp/src/views/projects/languages/MachineTranslation/getServiceName.tsx +++ b/webapp/src/views/projects/languages/MachineTranslation/getServiceName.tsx @@ -14,6 +14,8 @@ export const getServiceName = (service: ServiceType) => { return 'Tolgee'; case 'AWS': return 'Amazon Translate'; + case 'OPENAI': + return 'OpenAI'; default: return service; } diff --git a/webapp/src/views/projects/translations/ToolsPanel/panels/MachineTranslation/useServiceImg.ts b/webapp/src/views/projects/translations/ToolsPanel/panels/MachineTranslation/useServiceImg.ts index 40541ac4fd..d03f4f07dc 100644 --- a/webapp/src/views/projects/translations/ToolsPanel/panels/MachineTranslation/useServiceImg.ts +++ b/webapp/src/views/projects/translations/ToolsPanel/panels/MachineTranslation/useServiceImg.ts @@ -19,6 +19,8 @@ export const useServiceImg = () => { return contextPresent ? `/images/providers/tolgee-logo-${palette.mode}-in-context.svg` : `/images/providers/tolgee-logo-${palette.mode}.svg`; + case 'OPENAI': + return `/images/providers/openai-icon-${palette.mode}.svg`; default: return undefined; }