diff --git a/README.md b/README.md
index d792b5dd..5d6385b3 100644
--- a/README.md
+++ b/README.md
@@ -6,13 +6,20 @@ A translation app for GNOME.

+## Translation Providers
+
+- Proprietary:
+ - [Google Translate](https://translate.google.com/)
+ - [DeepL](https://www.deepl.com/en/translator) - Requires a Free or Paid API key.
+ - [Kagi Translate](https://translate.kagi.com/) - Requires session token (not complete URL) from [Kagi settings](https://kagi.com/settings/user_details) as API Key.
+ - [Microsoft Translator (Bing)](https://www.bing.com/translator)
+ - [Yandex Translate](https://translate.yandex.com/)
+- Open Source:
+ - LibreTranslate - Use any public instance, defaults to [our own](https://lt.dialectapp.org/).
+ - Lingva Translate - Use any public instance, defaults to [our own](https://lingva.dialectapp.org/).
+
## Features
-- Translation based on Google Translate
-- Translation based on the LibreTranslate API, allowing you to use any public instance
-- Translation based on Lingva Translate API
-- Translation based on Bing
-- Translation based on Yandex
- Translation history
- Automatic language detection
- Text to speech
diff --git a/data/app.drey.Dialect.metainfo.xml.in.in b/data/app.drey.Dialect.metainfo.xml.in.in
index 3159228a..0f88479f 100644
--- a/data/app.drey.Dialect.metainfo.xml.in.in
+++ b/data/app.drey.Dialect.metainfo.xml.in.in
@@ -9,16 +9,22 @@
A translation app for GNOME.
+
+ Translation Providers:
+
+
+ - Google Translate
+ - DeepL
+ - Kagi Translate
+ - Microsoft Translator (Bing)
+ - Yandex Translate
+ - Lingva Translate
+ - LibreTranslate
+
Features:
- - Translation based on Google Translate
- - Translation based on the LibreTranslate API, allowing you to use any public instance
- - Translation based on Lingva Translate API
- - Translation based on Bing
- - Translation based on Yandex
- - Translation based on DeepL
- Text to speech
- Translation history
- Automatic language detection
@@ -31,8 +37,6 @@
https://github.com/dialect-app/dialect/discussions/
https://hosted.weblate.org/engage/dialect/
https://github.com/dialect-app/dialect/
-
- The Dialect Authors
The Dialect Authors
diff --git a/dialect/providers/modules/deepl.py b/dialect/providers/modules/deepl.py
index 47ed9aee..56b529d7 100644
--- a/dialect/providers/modules/deepl.py
+++ b/dialect/providers/modules/deepl.py
@@ -85,8 +85,6 @@ async def validate_api_key(self, key):
return True
except (APIKeyInvalid, APIKeyRequired):
return False
- except Exception:
- raise
async def translate(self, request):
src, dest = self.denormalize_lang(request.src, request.dest)
diff --git a/dialect/providers/modules/kagi.py b/dialect/providers/modules/kagi.py
new file mode 100644
index 00000000..a6c10921
--- /dev/null
+++ b/dialect/providers/modules/kagi.py
@@ -0,0 +1,115 @@
+# Copyright 2025 Mufeed Ali
+# Copyright 2025 Rafael Mardojai CM
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from dialect.providers.base import (
+ ProviderCapability,
+ ProviderFeature,
+ ProviderLangComparison,
+ Translation,
+)
+from dialect.providers.errors import (
+ APIKeyRequired,
+ UnexpectedError,
+)
+from dialect.providers.soup import SoupProvider
+
+
+class Provider(SoupProvider):
+ name = "kagi"
+ prettyname = "Kagi Translate"
+
+ capabilities = ProviderCapability.TRANSLATION
+ features = ProviderFeature.DETECTION | ProviderFeature.API_KEY | ProviderFeature.API_KEY_REQUIRED
+ lang_comp = ProviderLangComparison.DEEP
+
+ defaults = {
+ "instance_url": "",
+ "api_key": "",
+ "src_langs": ["en", "fr", "es", "de", "ja", "zh"],
+ "dest_langs": ["fr", "es", "de", "en", "ja", "zh"],
+ }
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+
+ self.api_url = "translate.kagi.com/api"
+ self.chars_limit = 20000 # Web UI limit
+
+ @property
+ def headers(self):
+ return {"Content-Type": "application/json"}
+
+ @property
+ def lang_url(self):
+ return self.format_url(self.api_url, "/list-languages", params={"token": self.api_key})
+
+ @property
+ def translate_url(self):
+ return self.format_url(self.api_url, "/translate", params={"token": self.api_key})
+
+ async def validate_api_key(self, key):
+ """Validate the API key (session token)"""
+ try:
+ # Test session token by checking authentication status
+ url = self.format_url(self.api_url, "/auth", params={"token": key})
+ response = await self.get(url, self.headers)
+ if response and isinstance(response, dict) and response["loggedIn"] is True:
+ return True
+ except (APIKeyRequired, UnexpectedError):
+ return False
+ return False
+
+ async def init_trans(self):
+ """Initialize translation capabilities by fetching supported languages"""
+ languages = await self.get(self.lang_url, self.headers)
+
+ if languages and isinstance(languages, list):
+ for lang in languages:
+ # Add language with lowercase code as per Kagi API convention
+ self.add_lang(lang["language"].lower(), lang["name"])
+
+ async def translate(self, request):
+ """Translate text using Kagi API"""
+ src, dest = self.denormalize_lang(request.src, request.dest)
+
+ data = {
+ "text": request.text,
+ "source_lang": src if src != "auto" else "auto",
+ "target_lang": dest,
+ "skip_definition": True, # Get translation only, no definitions
+ }
+
+ response = await self.post(self.translate_url, data, self.headers)
+
+ if response and isinstance(response, dict):
+ detected = None
+ if "detected_language" in response and response["detected_language"]:
+ detected = response["detected_language"].get("iso")
+
+ translation = Translation(response["translation"], request, detected)
+ return translation
+
+ raise UnexpectedError("Failed reading the translation data")
+
+ def check_known_errors(self, status, data):
+ """Check for known error conditions in the response"""
+ if not data:
+ raise UnexpectedError("Response is empty")
+
+ # Check for error field in response
+ if isinstance(data, dict) and "error" in data:
+ error = data["error"]
+
+ if any(keyword in error.lower() for keyword in ["token", "unauthorized", "authentication"]):
+ raise APIKeyRequired(f"Invalid session token: {error}")
+ else:
+ raise UnexpectedError(error)
+
+ # Check HTTP status codes
+ if status == 401:
+ raise APIKeyRequired("Unauthorized - invalid session token")
+ elif status == 403:
+ raise APIKeyRequired("Forbidden - session token required")
+ elif status != 200:
+ raise UnexpectedError(f"HTTP {status} error")
diff --git a/dialect/providers/modules/libretrans.py b/dialect/providers/modules/libretrans.py
index 0a2b4ddd..79ddb88a 100644
--- a/dialect/providers/modules/libretrans.py
+++ b/dialect/providers/modules/libretrans.py
@@ -80,8 +80,6 @@ async def validate_api_key(self, key):
return "confidence" in response[0]
except (APIKeyInvalid, APIKeyRequired):
return False
- except Exception:
- raise
async def init_trans(self):
languages = await self.get(self.lang_url)