Skip to content

Commit 7d42909

Browse files
mufeedalirafaelmardojai
authored andcommitted
feat: Add Kagi as a provider
1 parent a1caf6e commit 7d42909

File tree

5 files changed

+139
-17
lines changed

5 files changed

+139
-17
lines changed

README.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,20 @@ A translation app for GNOME.
66

77
![Dialect](preview.png?raw=true)
88

9+
## Translation Providers
10+
11+
- Proprietary:
12+
- [Google Translate](https://translate.google.com/)
13+
- [DeepL](https://www.deepl.com/en/translator) - Requires a Free or Paid API key.
14+
- [Kagi Translate](https://translate.kagi.com/) - Requires session token (not complete URL) from [Kagi settings](https://kagi.com/settings/user_details) as API Key.
15+
- [Microsoft Translator (Bing)](https://www.bing.com/translator)
16+
- [Yandex Translate](https://translate.yandex.com/)
17+
- Open Source:
18+
- LibreTranslate - Use any public instance, defaults to [our own](https://lt.dialectapp.org/).
19+
- Lingva Translate - Use any public instance, defaults to [our own](https://lingva.dialectapp.org/).
20+
921
## Features
1022

11-
- Translation based on Google Translate
12-
- Translation based on the LibreTranslate API, allowing you to use any public instance
13-
- Translation based on Lingva Translate API</li>
14-
- Translation based on Bing
15-
- Translation based on Yandex
1623
- Translation history
1724
- Automatic language detection
1825
- Text to speech

data/app.drey.Dialect.metainfo.xml.in.in

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,22 @@
99
<p>
1010
A translation app for GNOME.
1111
</p>
12+
<p>
13+
Translation Providers:
14+
</p>
15+
<ul>
16+
<li>Google Translate</li>
17+
<li>DeepL</li>
18+
<li>Kagi Translate</li>
19+
<li>Microsoft Translator (Bing)</li>
20+
<li>Yandex Translate</li>
21+
<li>Lingva Translate</li>
22+
<li>LibreTranslate</li>
23+
</ul>
1224
<p>
1325
Features:
1426
</p>
1527
<ul>
16-
<li>Translation based on Google Translate</li>
17-
<li>Translation based on the LibreTranslate API, allowing you to use any public instance</li>
18-
<li>Translation based on Lingva Translate API</li>
19-
<li>Translation based on Bing</li>
20-
<li>Translation based on Yandex</li>
21-
<li>Translation based on DeepL</li>
2228
<li>Text to speech</li>
2329
<li>Translation history</li>
2430
<li>Automatic language detection</li>
@@ -31,8 +37,6 @@
3137
<url type="help">https://github.com/dialect-app/dialect/discussions/</url>
3238
<url type="translate">https://hosted.weblate.org/engage/dialect/</url>
3339
<url type="vcs-browser">https://github.com/dialect-app/dialect/</url>
34-
<!-- developer_name tag deprecated with Appstream 1.0 -->
35-
<developer_name>The Dialect Authors</developer_name>
3640
<developer id="org.dialectapp">
3741
<name>The Dialect Authors</name>
3842
</developer>

dialect/providers/modules/deepl.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,6 @@ async def validate_api_key(self, key):
8585
return True
8686
except (APIKeyInvalid, APIKeyRequired):
8787
return False
88-
except Exception:
89-
raise
9088

9189
async def translate(self, request):
9290
src, dest = self.denormalize_lang(request.src, request.dest)

dialect/providers/modules/kagi.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# Copyright 2025 Mufeed Ali
2+
# Copyright 2025 Rafael Mardojai CM
3+
# SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
from dialect.providers.base import (
6+
ProviderCapability,
7+
ProviderFeature,
8+
ProviderLangComparison,
9+
Translation,
10+
)
11+
from dialect.providers.errors import (
12+
APIKeyRequired,
13+
UnexpectedError,
14+
)
15+
from dialect.providers.soup import SoupProvider
16+
17+
18+
class Provider(SoupProvider):
19+
name = "kagi"
20+
prettyname = "Kagi Translate"
21+
22+
capabilities = ProviderCapability.TRANSLATION
23+
features = ProviderFeature.DETECTION | ProviderFeature.API_KEY | ProviderFeature.API_KEY_REQUIRED
24+
lang_comp = ProviderLangComparison.DEEP
25+
26+
defaults = {
27+
"instance_url": "",
28+
"api_key": "",
29+
"src_langs": ["en", "fr", "es", "de", "ja", "zh"],
30+
"dest_langs": ["fr", "es", "de", "en", "ja", "zh"],
31+
}
32+
33+
def __init__(self, **kwargs):
34+
super().__init__(**kwargs)
35+
36+
self.api_url = "translate.kagi.com/api"
37+
self.chars_limit = 20000 # Web UI limit
38+
39+
@property
40+
def headers(self):
41+
return {"Content-Type": "application/json"}
42+
43+
@property
44+
def lang_url(self):
45+
return self.format_url(self.api_url, "/list-languages", params={"token": self.api_key})
46+
47+
@property
48+
def translate_url(self):
49+
return self.format_url(self.api_url, "/translate", params={"token": self.api_key})
50+
51+
async def validate_api_key(self, key):
52+
"""Validate the API key (session token)"""
53+
try:
54+
# Test session token by checking authentication status
55+
url = self.format_url(self.api_url, "/auth", params={"token": key})
56+
response = await self.get(url, self.headers)
57+
if response and isinstance(response, dict) and response["loggedIn"] is True:
58+
return True
59+
except (APIKeyRequired, UnexpectedError):
60+
return False
61+
return False
62+
63+
async def init_trans(self):
64+
"""Initialize translation capabilities by fetching supported languages"""
65+
languages = await self.get(self.lang_url, self.headers)
66+
67+
if languages and isinstance(languages, list):
68+
for lang in languages:
69+
# Add language with lowercase code as per Kagi API convention
70+
self.add_lang(lang["language"].lower(), lang["name"])
71+
72+
async def translate(self, request):
73+
"""Translate text using Kagi API"""
74+
src, dest = self.denormalize_lang(request.src, request.dest)
75+
76+
data = {
77+
"text": request.text,
78+
"source_lang": src if src != "auto" else "auto",
79+
"target_lang": dest,
80+
"skip_definition": True, # Get translation only, no definitions
81+
}
82+
83+
response = await self.post(self.translate_url, data, self.headers)
84+
85+
if response and isinstance(response, dict):
86+
detected = None
87+
if "detected_language" in response and response["detected_language"]:
88+
detected = response["detected_language"].get("iso")
89+
90+
translation = Translation(response["translation"], request, detected)
91+
return translation
92+
93+
raise UnexpectedError("Failed reading the translation data")
94+
95+
def check_known_errors(self, status, data):
96+
"""Check for known error conditions in the response"""
97+
if not data:
98+
raise UnexpectedError("Response is empty")
99+
100+
# Check for error field in response
101+
if isinstance(data, dict) and "error" in data:
102+
error = data["error"]
103+
104+
if any(keyword in error.lower() for keyword in ["token", "unauthorized", "authentication"]):
105+
raise APIKeyRequired(f"Invalid session token: {error}")
106+
else:
107+
raise UnexpectedError(error)
108+
109+
# Check HTTP status codes
110+
if status == 401:
111+
raise APIKeyRequired("Unauthorized - invalid session token")
112+
elif status == 403:
113+
raise APIKeyRequired("Forbidden - session token required")
114+
elif status != 200:
115+
raise UnexpectedError(f"HTTP {status} error")

dialect/providers/modules/libretrans.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,6 @@ async def validate_api_key(self, key):
8080
return "confidence" in response[0]
8181
except (APIKeyInvalid, APIKeyRequired):
8282
return False
83-
except Exception:
84-
raise
8583

8684
async def init_trans(self):
8785
languages = await self.get(self.lang_url)

0 commit comments

Comments
 (0)