Skip to content
Merged
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
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,20 @@ A translation app for GNOME.

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

## 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</li>
- Translation based on Bing
- Translation based on Yandex
- Translation history
- Automatic language detection
- Text to speech
Expand Down
20 changes: 12 additions & 8 deletions data/app.drey.Dialect.metainfo.xml.in.in
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,22 @@
<p>
A translation app for GNOME.
</p>
<p>
Translation Providers:
</p>
<ul>
<li>Google Translate</li>
<li>DeepL</li>
<li>Kagi Translate</li>
<li>Microsoft Translator (Bing)</li>
<li>Yandex Translate</li>
<li>Lingva Translate</li>
<li>LibreTranslate</li>
</ul>
<p>
Features:
</p>
<ul>
<li>Translation based on Google Translate</li>
<li>Translation based on the LibreTranslate API, allowing you to use any public instance</li>
<li>Translation based on Lingva Translate API</li>
<li>Translation based on Bing</li>
<li>Translation based on Yandex</li>
<li>Translation based on DeepL</li>
<li>Text to speech</li>
<li>Translation history</li>
<li>Automatic language detection</li>
Expand All @@ -31,8 +37,6 @@
<url type="help">https://github.com/dialect-app/dialect/discussions/</url>
<url type="translate">https://hosted.weblate.org/engage/dialect/</url>
<url type="vcs-browser">https://github.com/dialect-app/dialect/</url>
<!-- developer_name tag deprecated with Appstream 1.0 -->
<developer_name>The Dialect Authors</developer_name>
<developer id="org.dialectapp">
<name>The Dialect Authors</name>
</developer>
Expand Down
2 changes: 0 additions & 2 deletions dialect/providers/modules/deepl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
115 changes: 115 additions & 0 deletions dialect/providers/modules/kagi.py
Original file line number Diff line number Diff line change
@@ -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")
2 changes: 0 additions & 2 deletions dialect/providers/modules/libretrans.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down