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
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using Translumo.Infrastructure.Language;
using Translumo.Utils;

Expand Down Expand Up @@ -40,6 +40,42 @@ public Translators Translator
}
}

public string DeepseekApiKey
{
get => _deepseekApiKey;
set
{
SetProperty(ref _deepseekApiKey, value);
}
}

public string GeminiApiKey
{
get => _geminiApiKey;
set
{
SetProperty(ref _geminiApiKey, value);
}
}

public string OpenrouterApiKey
{
get => _openrouterApiKey;
set
{
SetProperty(ref _openrouterApiKey, value);
}
}

public string OpenrouterModel
{
get => _openrouterModel;
set
{
SetProperty(ref _openrouterModel, value);
}
}

public List<Proxy> ProxySettings
{
get => _proxySettings;
Expand All @@ -52,6 +88,10 @@ public List<Proxy> ProxySettings
private Languages _translateFromLang;
private Languages _translateToLang;
private Translators _translator;
private string _deepseekApiKey;
private string _geminiApiKey;
private string _openrouterApiKey;
private string _openrouterModel = "nvidia/nemotron-3.5-content-safety:free";
private List<Proxy> _proxySettings = new List<Proxy>();
}
}
37 changes: 37 additions & 0 deletions src/Translumo.Translation/Deepseek/DeepseekContainer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.Net;
using Translumo.Translation.Configuration;
using Translumo.Utils.Http;

namespace Translumo.Translation.Deepseek
{
public sealed class DeepseekContainer : TranslationContainer
{
public HttpReader Reader { get; private set; }
public string ApiKey { get; private set; }

public DeepseekContainer(string apiKey, Proxy proxy = null, bool isPrimary = false) : base(proxy, isPrimary)
{
ApiKey = apiKey;
Reader = CreateReader(proxy);
}

public override void Reset()
{
base.Reset();
Reader.Cookies = new CookieContainer();
}

private HttpReader CreateReader(Proxy proxy)
{
var reader = new HttpReader();
reader.ContentType = "application/json";
reader.Accept = "application/json";
if (!string.IsNullOrEmpty(ApiKey))
{
reader.OptionalHeaders.Add("Authorization", $"Bearer {ApiKey}");
}
reader.Proxy = proxy?.ToWebProxy();
return reader;
}
}
}
68 changes: 68 additions & 0 deletions src/Translumo.Translation/Deepseek/DeepseekTranslator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Translumo.Infrastructure.Language;
using Translumo.Translation.Configuration;
using Translumo.Translation.Exceptions;
using Translumo.Utils.Http;

namespace Translumo.Translation.Deepseek
{
public sealed class DeepseekTranslator : BaseTranslator<DeepseekContainer>
{
private const string API_URL = "https://api.deepseek.com/chat/completions";

public DeepseekTranslator(TranslationConfiguration translationConfiguration, LanguageService languageService, ILogger logger)
: base(translationConfiguration, languageService, logger)
{
}

protected override async Task<string> TranslateTextInternal(DeepseekContainer container, string sourceText)
{
if (string.IsNullOrEmpty(container.ApiKey))
{
throw new TranslationException("Deepseek API Key is not configured.");
}

var payload = new
{
model = "deepseek-chat",
messages = new[]
{
new { role = "system", content = $"You are a highly accurate translator. Translate the following text from {SourceLangDescriptor.IsoCode} to {TargetLangDescriptor.IsoCode}. Reply ONLY with the translation, no extra text, no explanations." },
new { role = "user", content = sourceText }
},
temperature = 0.3
};

var dataIn = JsonSerializer.Serialize(payload);
HttpResponse httpResponse = await container.Reader.RequestWebDataAsync(API_URL, HttpMethods.POST, dataIn, acceptCookie: false).ConfigureAwait(false);

if (httpResponse.IsSuccessful)
{
try
{
using var doc = JsonDocument.Parse(httpResponse.Body);
var translation = doc.RootElement.GetProperty("choices")[0].GetProperty("message").GetProperty("content").GetString();
return translation.Trim();
}
catch (Exception ex)
{
throw new TranslationException($"Failed to parse Deepseek response: '{httpResponse.Body}'", ex);
}
}

throw new TranslationException($"Deepseek API returned error: '{httpResponse.Body}'", httpResponse.InnerException);
}

protected override IList<DeepseekContainer> CreateContainers(TranslationConfiguration configuration)
{
var result = configuration.ProxySettings.Select(proxy => new DeepseekContainer(configuration.DeepseekApiKey, proxy)).ToList();
result.Add(new DeepseekContainer(configuration.DeepseekApiKey, isPrimary: true));
return result;
}
}
}
33 changes: 33 additions & 0 deletions src/Translumo.Translation/Gemini/GeminiContainer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Net;
using Translumo.Translation.Configuration;
using Translumo.Utils.Http;

namespace Translumo.Translation.Gemini
{
public sealed class GeminiContainer : TranslationContainer
{
public HttpReader Reader { get; private set; }
public string ApiKey { get; private set; }

public GeminiContainer(string apiKey, Proxy proxy = null, bool isPrimary = false) : base(proxy, isPrimary)
{
ApiKey = apiKey;
Reader = CreateReader(proxy);
}

public override void Reset()
{
base.Reset();
Reader.Cookies = new CookieContainer();
}

private HttpReader CreateReader(Proxy proxy)
{
var reader = new HttpReader();
reader.ContentType = "application/json";
reader.Accept = "application/json";
reader.Proxy = proxy?.ToWebProxy();
return reader;
}
}
}
76 changes: 76 additions & 0 deletions src/Translumo.Translation/Gemini/GeminiTranslator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Translumo.Infrastructure.Language;
using Translumo.Translation.Configuration;
using Translumo.Translation.Exceptions;
using Translumo.Utils.Http;

namespace Translumo.Translation.Gemini
{
public sealed class GeminiTranslator : BaseTranslator<GeminiContainer>
{
private const string API_URL_TEMPLATE = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={0}";

public GeminiTranslator(TranslationConfiguration translationConfiguration, LanguageService languageService, ILogger logger)
: base(translationConfiguration, languageService, logger)
{
}

protected override async Task<string> TranslateTextInternal(GeminiContainer container, string sourceText)
{
if (string.IsNullOrEmpty(container.ApiKey))
{
throw new TranslationException("Gemini API Key is not configured.");
}

var payload = new
{
contents = new[]
{
new
{
parts = new[]
{
new { text = $"You are a highly accurate translator. Translate the following text from {SourceLangDescriptor.IsoCode} to {TargetLangDescriptor.IsoCode}. Reply ONLY with the translation, no extra text, no explanations.\n\nText: {sourceText}" }
}
}
}
};

var dataIn = JsonSerializer.Serialize(payload);
var apiUrl = string.Format(API_URL_TEMPLATE, container.ApiKey);
HttpResponse httpResponse = await container.Reader.RequestWebDataAsync(apiUrl, HttpMethods.POST, dataIn, acceptCookie: false).ConfigureAwait(false);

if (httpResponse.IsSuccessful)
{
try
{
using var doc = JsonDocument.Parse(httpResponse.Body);
var candidates = doc.RootElement.GetProperty("candidates");
if (candidates.GetArrayLength() > 0)
{
var translation = candidates[0].GetProperty("content").GetProperty("parts")[0].GetProperty("text").GetString();
return translation.Trim();
}
}
catch (Exception ex)
{
throw new TranslationException($"Failed to parse Gemini response: '{httpResponse.Body}'", ex);
}
}

throw new TranslationException($"Gemini API returned error: '{httpResponse.Body}'", httpResponse.InnerException);
}

protected override IList<GeminiContainer> CreateContainers(TranslationConfiguration configuration)
{
var result = configuration.ProxySettings.Select(proxy => new GeminiContainer(configuration.GeminiApiKey, proxy)).ToList();
result.Add(new GeminiContainer(configuration.GeminiApiKey, isPrimary: true));
return result;
}
}
}
39 changes: 39 additions & 0 deletions src/Translumo.Translation/Openrouter/OpenrouterContainer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Net;
using Translumo.Translation.Configuration;
using Translumo.Utils.Http;

namespace Translumo.Translation.Openrouter
{
public sealed class OpenrouterContainer : TranslationContainer
{
public HttpReader Reader { get; private set; }
public string ApiKey { get; private set; }

public OpenrouterContainer(string apiKey, Proxy proxy = null, bool isPrimary = false) : base(proxy, isPrimary)
{
ApiKey = apiKey;
Reader = CreateReader(proxy);
}

public override void Reset()
{
base.Reset();
Reader.Cookies = new CookieContainer();
}

private HttpReader CreateReader(Proxy proxy)
{
var reader = new HttpReader();
reader.ContentType = "application/json";
reader.Accept = "application/json";
if (!string.IsNullOrEmpty(ApiKey))
{
reader.OptionalHeaders.Add("Authorization", $"Bearer {ApiKey}");
}
reader.OptionalHeaders.Add("HTTP-Referer", "https://github.com/Translumo");
reader.OptionalHeaders.Add("X-Title", "Translumo");
reader.Proxy = proxy?.ToWebProxy();
return reader;
}
}
}
72 changes: 72 additions & 0 deletions src/Translumo.Translation/Openrouter/OpenrouterTranslator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Translumo.Infrastructure.Language;
using Translumo.Translation.Configuration;
using Translumo.Translation.Exceptions;
using Translumo.Utils.Http;

namespace Translumo.Translation.Openrouter
{
public sealed class OpenrouterTranslator : BaseTranslator<OpenrouterContainer>
{
private const string API_URL = "https://openrouter.ai/api/v1/chat/completions";

public OpenrouterTranslator(TranslationConfiguration translationConfiguration, LanguageService languageService, ILogger logger)
: base(translationConfiguration, languageService, logger)
{
}

protected override async Task<string> TranslateTextInternal(OpenrouterContainer container, string sourceText)
{
if (string.IsNullOrEmpty(container.ApiKey))
{
throw new TranslationException("Openrouter API Key is not configured.");
}

var modelName = !string.IsNullOrWhiteSpace(TranslationConfiguration.OpenrouterModel)
? TranslationConfiguration.OpenrouterModel
: "nvidia/nemotron-3.5-content-safety:free";

var payload = new
{
model = modelName,
messages = new[]
{
new { role = "system", content = $"You are a highly accurate translator. Translate the following text from {SourceLangDescriptor.IsoCode} to {TargetLangDescriptor.IsoCode}. Reply ONLY with the translation, no extra text, no explanations." },
new { role = "user", content = sourceText }
},
temperature = 0.3
};

var dataIn = JsonSerializer.Serialize(payload);
HttpResponse httpResponse = await container.Reader.RequestWebDataAsync(API_URL, HttpMethods.POST, dataIn, acceptCookie: false).ConfigureAwait(false);

if (httpResponse.IsSuccessful)
{
try
{
using var doc = JsonDocument.Parse(httpResponse.Body);
var translation = doc.RootElement.GetProperty("choices")[0].GetProperty("message").GetProperty("content").GetString();
return translation.Trim();
}
catch (Exception ex)
{
throw new TranslationException($"Failed to parse Openrouter response: '{httpResponse.Body}'", ex);
}
}

throw new TranslationException($"Openrouter API returned error: '{httpResponse.Body}'", httpResponse.InnerException);
}

protected override IList<OpenrouterContainer> CreateContainers(TranslationConfiguration configuration)
{
var result = configuration.ProxySettings.Select(proxy => new OpenrouterContainer(configuration.OpenrouterApiKey, proxy)).ToList();
result.Add(new OpenrouterContainer(configuration.OpenrouterApiKey, isPrimary: true));
return result;
}
}
}
Loading