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
21 changes: 11 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ A powerful async-first localization engine that supports various content types i
- 🔀 **Concurrent processing** for dramatically faster bulk translations
- 🎯 **Multiple content types**: text, objects, chat messages, and more
- 🌐 **Auto-detection** of source languages
- ⚡ **Fast mode** for quick translations
- 🔧 **Flexible configuration** with progress callbacks
- 📦 **Context manager** support for proper resource management

Expand Down Expand Up @@ -39,6 +38,7 @@ async def main():
result = await LingoDotDevEngine.quick_translate(
"Hello, world!",
api_key="your-api-key",
engine_id="your-engine-id",
target_locale="es"
)
print(result) # "¡Hola, mundo!"
Expand All @@ -55,9 +55,9 @@ from lingodotdev import LingoDotDevEngine
async def main():
config = {
"api_key": "your-api-key",
"api_url": "https://engine.lingo.dev" # Optional, defaults to this
"engine_id": "your-engine-id", # Optional
}

async with LingoDotDevEngine(config) as engine:
# Translate text
text_result = await engine.localize_text(
Expand Down Expand Up @@ -89,6 +89,7 @@ async def batch_example():
results = await LingoDotDevEngine.quick_batch_translate(
"Welcome to our application",
api_key="your-api-key",
engine_id="your-engine-id",
target_locales=["es", "fr", "de", "it"]
)
# Results: ["Bienvenido...", "Bienvenue...", "Willkommen...", "Benvenuto..."]
Expand All @@ -103,7 +104,7 @@ async def progress_example():

large_content = {f"item_{i}": f"Content {i}" for i in range(1000)}

async with LingoDotDevEngine({"api_key": "your-api-key"}) as engine:
async with LingoDotDevEngine({"api_key": "your-api-key", "engine_id": "your-engine-id"}) as engine:
result = await engine.localize_object(
large_content,
{"target_locale": "es"},
Expand All @@ -122,7 +123,7 @@ async def chat_example():
{"name": "Charlie", "text": "Great to see you all!"}
]

async with LingoDotDevEngine({"api_key": "your-api-key"}) as engine:
async with LingoDotDevEngine({"api_key": "your-api-key", "engine_id": "your-engine-id"}) as engine:
translated_chat = await engine.localize_chat(
chat_messages,
{"source_locale": "en", "target_locale": "es"}
Expand All @@ -140,7 +141,7 @@ async def concurrent_objects_example():
{"success": "Account created", "next": "Continue to dashboard"}
]

async with LingoDotDevEngine({"api_key": "your-api-key"}) as engine:
async with LingoDotDevEngine({"api_key": "your-api-key", "engine_id": "your-engine-id"}) as engine:
results = await engine.batch_localize_objects(
objects,
{"target_locale": "fr"}
Expand All @@ -152,7 +153,7 @@ async def concurrent_objects_example():

```python
async def detection_example():
async with LingoDotDevEngine({"api_key": "your-api-key"}) as engine:
async with LingoDotDevEngine({"api_key": "your-api-key", "engine_id": "your-engine-id"}) as engine:
detected = await engine.recognize_locale("Bonjour le monde")
print(detected) # "fr"
```
Expand All @@ -162,7 +163,8 @@ async def detection_example():
```python
config = {
"api_key": "your-api-key", # Required: Your API key
"api_url": "https://engine.lingo.dev", # Optional: API endpoint
"engine_id": "your-engine-id", # Optional: Your engine ID
"api_url": "https://api.lingo.dev", # Optional: API endpoint
"batch_size": 25, # Optional: Items per batch (1-250)
"ideal_batch_item_size": 250 # Optional: Target words per batch (1-2500)
}
Expand All @@ -173,7 +175,6 @@ config = {
### Translation Parameters
- **source_locale**: Source language code (auto-detected if None)
- **target_locale**: Target language code (required)
- **fast**: Enable fast mode for quicker translations
- **reference**: Reference translations for context
- **concurrent**: Process chunks concurrently (faster, but no progress callbacks)

Expand All @@ -186,7 +187,7 @@ config = {
```python
async def error_handling_example():
try:
async with LingoDotDevEngine({"api_key": "invalid-key"}) as engine:
async with LingoDotDevEngine({"api_key": "invalid-key", "engine_id": "your-engine-id"}) as engine:
result = await engine.localize_text("Hello", {"target_locale": "es"})
except ValueError as e:
print(f"Invalid request: {e}")
Expand Down
63 changes: 42 additions & 21 deletions src/lingodotdev/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import asyncio
import json
from typing import Any, Callable, Dict, List, Optional
from urllib.parse import urljoin

import httpx
from nanoid import generate
Expand All @@ -18,16 +17,27 @@ class EngineConfig(BaseModel):
"""Configuration for the LingoDotDevEngine"""

api_key: str
api_url: str = "https://engine.lingo.dev"
engine_id: Optional[str] = None
api_url: str = "https://api.lingo.dev"
batch_size: int = Field(default=25, ge=1, le=250)
ideal_batch_item_size: int = Field(default=250, ge=1, le=2500)

@validator("engine_id", pre=True, always=True)
@classmethod
def validate_engine_id(cls, v: Optional[str]) -> Optional[str]:
if v is None:
return None
stripped = v.strip()
if not stripped:
return None
return stripped

@validator("api_url")
@classmethod
def validate_api_url(cls, v: str) -> str:
if not v.startswith(("http://", "https://")):
raise ValueError("API URL must be a valid HTTP/HTTPS URL")
return v
return v.rstrip("/")


class LocalizationParams(BaseModel):
Expand Down Expand Up @@ -55,6 +65,7 @@ def __init__(self, config: Dict[str, Any]):
"""
self.config = EngineConfig(**config)
self._client: Optional[httpx.AsyncClient] = None
self._session_id: str = generate()

async def __aenter__(self):
"""Async context manager entry"""
Expand All @@ -71,7 +82,7 @@ async def _ensure_client(self):
self._client = httpx.AsyncClient(
headers={
"Content-Type": "application/json; charset=utf-8",
"Authorization": f"Bearer {self.config.api_key}",
"X-API-Key": self.config.api_key,
},
timeout=60.0,
)
Expand Down Expand Up @@ -134,7 +145,6 @@ async def _localize_raw(
"""
await self._ensure_client()
chunked_payload = self._extract_payload_chunks(payload)
workflow_id = generate()

if concurrent and not progress_callback:
# Process chunks concurrently for better performance
Expand All @@ -144,7 +154,6 @@ async def _localize_raw(
params.source_locale,
params.target_locale,
{"data": chunk, "reference": params.reference},
workflow_id,
params.fast or False,
)
tasks.append(task)
Expand All @@ -160,7 +169,6 @@ async def _localize_raw(
params.source_locale,
params.target_locale,
{"data": chunk, "reference": params.reference},
workflow_id,
params.fast or False,
)

Expand All @@ -182,7 +190,6 @@ async def _localize_chunk(
source_locale: Optional[str],
target_locale: str,
payload: Dict[str, Any],
workflow_id: str,
fast: bool,
) -> Dict[str, str]:
"""
Expand All @@ -192,22 +199,24 @@ async def _localize_chunk(
source_locale: Source locale
target_locale: Target locale
payload: Payload containing the chunk to be localized
workflow_id: Workflow ID for tracking
fast: Whether to use fast mode

Returns:
Localized chunk
"""
await self._ensure_client()
assert self._client is not None # Type guard for mypy
url = urljoin(self.config.api_url, "/i18n")

request_data = {
"params": {"workflowId": workflow_id, "fast": fast},
"locale": {"source": source_locale, "target": target_locale},
url = f"{self.config.api_url}/process/localize"
request_data: Dict[str, Any] = {
"params": {"fast": fast},
"sourceLocale": source_locale,
"targetLocale": target_locale,
"data": payload["data"],
"sessionId": self._session_id,
}

if self.config.engine_id:
request_data["engineId"] = self.config.engine_id
if payload.get("reference"):
request_data["reference"] = payload["reference"]

Expand Down Expand Up @@ -455,7 +464,8 @@ async def recognize_locale(self, text: str) -> str:

await self._ensure_client()
assert self._client is not None # Type guard for mypy
url = urljoin(self.config.api_url, "/recognize")

url = f"{self.config.api_url}/process/recognize"

try:
response = await self._client.post(url, json={"text": text})
Expand Down Expand Up @@ -487,10 +497,11 @@ async def whoami(self) -> Optional[Dict[str, str]]:
"""
await self._ensure_client()
assert self._client is not None # Type guard for mypy
url = urljoin(self.config.api_url, "/whoami")

url = f"{self.config.api_url}/users/me"

try:
response = await self._client.post(url)
response = await self._client.get(url)

if response.is_success:
payload = self._safe_parse_json(response)
Expand Down Expand Up @@ -539,7 +550,8 @@ async def quick_translate(
api_key: str,
target_locale: str,
source_locale: Optional[str] = None,
api_url: str = "https://engine.lingo.dev",
engine_id: Optional[str] = None,
api_url: str = "https://api.lingo.dev",
fast: bool = True,
) -> Any:
"""
Expand All @@ -551,6 +563,7 @@ async def quick_translate(
api_key: Your Lingo.dev API key
target_locale: Target language code (e.g., 'es', 'fr')
source_locale: Source language code (optional, auto-detected if None)
engine_id: Optional engine ID for the API
api_url: API endpoint URL
fast: Enable fast mode for quicker translations

Expand All @@ -562,20 +575,24 @@ async def quick_translate(
result = await LingoDotDevEngine.quick_translate(
"Hello world",
"your-api-key",
"your-engine-id",
"es"
)

# Translate object
result = await LingoDotDevEngine.quick_translate(
{"greeting": "Hello", "farewell": "Goodbye"},
"your-api-key",
"your-engine-id",
"es"
)
"""
config = {
config: Dict[str, Any] = {
"api_key": api_key,
"api_url": api_url,
}
if engine_id:
config["engine_id"] = engine_id

async with cls(config) as engine:
params = {
Expand All @@ -598,7 +615,8 @@ async def quick_batch_translate(
api_key: str,
target_locales: List[str],
source_locale: Optional[str] = None,
api_url: str = "https://engine.lingo.dev",
engine_id: Optional[str] = None,
api_url: str = "https://api.lingo.dev",
fast: bool = True,
) -> List[Any]:
"""
Expand All @@ -610,6 +628,7 @@ async def quick_batch_translate(
api_key: Your Lingo.dev API key
target_locales: List of target language codes (e.g., ['es', 'fr', 'de'])
source_locale: Source language code (optional, auto-detected if None)
engine_id: Optional engine ID for the API
api_url: API endpoint URL
fast: Enable fast mode for quicker translations

Expand All @@ -624,10 +643,12 @@ async def quick_batch_translate(
)
# Results: ["Hola mundo", "Bonjour le monde", "Hallo Welt"]
"""
config = {
config: Dict[str, Any] = {
"api_key": api_key,
"api_url": api_url,
}
if engine_id:
config["engine_id"] = engine_id

async with cls(config) as engine:
if isinstance(content, str):
Expand Down
Loading