Skip to content

Commit 42ad450

Browse files
committed
feat: Add Kagi as a provider
1 parent a1caf6e commit 42ad450

File tree

4 files changed

+138
-13
lines changed

4 files changed

+138
-13
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+
- [Bing](https://www.bing.com/translator)
16+
- [Yandex](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

dialect/providers/modules/deepl.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
# Copyright 2024 Mufeed Ali
2-
# Copyright 2024 Rafael Mardojai CM
1+
# Copyright 2025 Mufeed Ali
2+
# Copyright 2025 Rafael Mardojai CM
33
# SPDX-License-Identifier: GPL-3.0-or-later
44

55
from dialect.providers.base import ProviderCapability, ProviderFeature, ProviderLangComparison, Translation
@@ -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: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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+
api_url = "translate.kagi.com"
34+
35+
def __init__(self, **kwargs):
36+
super().__init__(**kwargs)
37+
self.chars_limit = 5000 # Reasonable default for Kagi
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, "/api/list-languages")
46+
47+
@property
48+
def translate_url(self):
49+
return self.format_url(self.api_url, "/api/translate")
50+
51+
async def validate_api_key(self, key):
52+
"""Validate the API key (session token)"""
53+
try:
54+
# Test API key by attempting to get languages list
55+
url = f"{self.lang_url}?token={key}"
56+
await self.get(url, self.headers)
57+
return True
58+
except (APIKeyRequired, UnexpectedError):
59+
return False
60+
61+
async def init_trans(self):
62+
"""Initialize translation capabilities by fetching supported languages"""
63+
# Get languages with session token as query parameter
64+
url = f"{self.lang_url}?token={self.api_key}"
65+
66+
languages = await self.get(url, self.headers)
67+
68+
if languages and isinstance(languages, list):
69+
for lang in languages:
70+
# Add language with lowercase code as per Kagi API convention
71+
self.add_lang(lang["language"].lower(), lang["name"])
72+
73+
async def translate(self, request):
74+
"""Translate text using Kagi API"""
75+
# Check if API key (session token) is provided
76+
src, dest = self.denormalize_lang(request.src, request.dest)
77+
78+
# Request body following Kagi API structure
79+
data = {
80+
"text": request.text,
81+
"source_lang": src if src != "auto" else "auto",
82+
"target_lang": dest,
83+
"skip_definition": True, # Get translation only, no definitions
84+
}
85+
86+
# Add session token as query parameter
87+
url = f"{self.translate_url}?token={self.api_key}"
88+
89+
# Do request
90+
response = await self.post(url, data, self.headers)
91+
92+
if response and isinstance(response, dict):
93+
detected = None
94+
if "detected_language" in response and response["detected_language"]:
95+
detected = response["detected_language"].get("iso")
96+
97+
translation = Translation(response["translation"], request, detected)
98+
return translation
99+
100+
raise UnexpectedError("Failed reading the translation data")
101+
102+
def check_known_errors(self, status, data):
103+
"""Check for known error conditions in the response"""
104+
if not data:
105+
raise UnexpectedError("Response is empty")
106+
107+
# Check for error field in response
108+
if isinstance(data, dict) and "error" in data:
109+
error = data["error"]
110+
111+
if any(keyword in error.lower() for keyword in ["token", "unauthorized", "authentication"]):
112+
raise APIKeyRequired(f"Invalid session token: {error}")
113+
else:
114+
raise UnexpectedError(error)
115+
116+
# Check HTTP status codes
117+
if status == 401:
118+
raise APIKeyRequired("Unauthorized - invalid session token")
119+
elif status == 403:
120+
raise APIKeyRequired("Forbidden - session token required")
121+
elif status != 200:
122+
raise UnexpectedError(f"HTTP {status} error")

dialect/providers/modules/libretrans.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
# Copyright 2021 Mufeed Ali
2-
# Copyright 2021 Rafael Mardojai CM
1+
# Copyright 2025 Mufeed Ali
2+
# Copyright 2025 Rafael Mardojai CM
33
# SPDX-License-Identifier: GPL-3.0-or-later
44

55
from dialect.providers.base import (
@@ -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)