diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index 4a271c5..664d4bb 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
+ python-version: ['3.9', '3.10', '3.11', '3.12']
steps:
- name: Checkout code
@@ -67,4 +67,4 @@ jobs:
file: ./coverage.xml
flags: unittests
name: codecov-umbrella
- fail_ci_if_error: false
\ No newline at end of file
+ fail_ci_if_error: false
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index c92e30a..2c87c6f 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
+ python-version: ['3.9', '3.10', '3.11', '3.12']
steps:
- name: Checkout code
@@ -118,4 +118,4 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
- semantic-release publish
\ No newline at end of file
+ semantic-release publish
diff --git a/.github/workflows/update-docs.yml b/.github/workflows/update-docs.yml
new file mode 100644
index 0000000..6c8c2f6
--- /dev/null
+++ b/.github/workflows/update-docs.yml
@@ -0,0 +1,42 @@
+name: Regenerate API Docs
+
+on:
+ push:
+ paths:
+ - 'src/**'
+ - 'pyproject.toml'
+ - 'pydoc-markdown.yml'
+ workflow_dispatch:
+
+permissions:
+ contents: write
+
+jobs:
+ build-docs:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.12'
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install .[dev]
+
+ - name: Regenerate Markdown API docs
+ run: |
+ mkdir -p docs
+ pydoc-markdown
+
+ - name: Commit updated docs
+ uses: stefanzweifel/git-auto-commit-action@v5
+ with:
+ commit_message: "chore(docs): update generated API docs"
+ file_pattern: docs/*.md
diff --git a/docs/api.md b/docs/api.md
new file mode 100644
index 0000000..0448e5a
--- /dev/null
+++ b/docs/api.md
@@ -0,0 +1,636 @@
+# Table of Contents
+
+* [lingodotdev.engine](#lingodotdev.engine)
+ * [EngineConfig](#lingodotdev.engine.EngineConfig)
+ * [validate\_api\_url](#lingodotdev.engine.EngineConfig.validate_api_url)
+ * [LocalizationParams](#lingodotdev.engine.LocalizationParams)
+ * [validate\_locale](#lingodotdev.engine.LocalizationParams.validate_locale)
+ * [LingoDotDevEngine](#lingodotdev.engine.LingoDotDevEngine)
+ * [\_\_init\_\_](#lingodotdev.engine.LingoDotDevEngine.__init__)
+ * [\_\_aenter\_\_](#lingodotdev.engine.LingoDotDevEngine.__aenter__)
+ * [\_\_aexit\_\_](#lingodotdev.engine.LingoDotDevEngine.__aexit__)
+ * [close](#lingodotdev.engine.LingoDotDevEngine.close)
+ * [localize\_object](#lingodotdev.engine.LingoDotDevEngine.localize_object)
+ * [localize\_text](#lingodotdev.engine.LingoDotDevEngine.localize_text)
+ * [batch\_localize\_text](#lingodotdev.engine.LingoDotDevEngine.batch_localize_text)
+ * [localize\_chat](#lingodotdev.engine.LingoDotDevEngine.localize_chat)
+ * [recognize\_locale](#lingodotdev.engine.LingoDotDevEngine.recognize_locale)
+ * [whoami](#lingodotdev.engine.LingoDotDevEngine.whoami)
+ * [batch\_localize\_objects](#lingodotdev.engine.LingoDotDevEngine.batch_localize_objects)
+ * [quick\_translate](#lingodotdev.engine.LingoDotDevEngine.quick_translate)
+ * [quick\_batch\_translate](#lingodotdev.engine.LingoDotDevEngine.quick_batch_translate)
+
+
+
+# lingodotdev.engine
+
+Async client implementation for the Lingo.dev localization service.
+
+This module provides LingoDotDevEngine and supporting data models for
+configuration and localization parameter validation.
+
+ config = {"api_key": "your-api-key"}
+ async with LingoDotDevEngine(config) as engine:
+ result = await engine.localize_text("Hello", {"target_locale": "es"})
+
+
+
+## EngineConfig Objects
+
+```python
+class EngineConfig(BaseModel)
+```
+
+Runtime configuration for LingoDotDevEngine.
+
+Stores and validates configuration parameters required to interact with the
+Lingo.dev API.
+
+**Attributes**:
+
+- `api_key` - Secret token used to authenticate with the Lingo.dev API.
+- `api_url` - Base endpoint for the localization engine. Defaults to
+ 'https://engine.lingo.dev'.
+- `batch_size` - Maximum number of top-level entries to send in a single
+ localization request (between 1 and 250 inclusive).
+- `ideal_batch_item_size` - Target word count per request before payloads are
+ split into multiple batches (between 1 and 2500 inclusive).
+
+
+
+### validate\_api\_url
+
+```python
+@validator("api_url")
+@classmethod
+def validate_api_url(cls, v: str) -> str
+```
+
+Validates that the API URL is a valid HTTP/HTTPS URL.
+
+**Arguments**:
+
+- `v` - The URL string to validate.
+
+
+**Returns**:
+
+ The validated URL string.
+
+
+**Raises**:
+
+- `ValueError` - If the URL doesn't start with http:// or https://.
+
+
+
+## LocalizationParams Objects
+
+```python
+class LocalizationParams(BaseModel)
+```
+
+Request parameters for localization operations.
+
+These values are serialized directly into API requests after validation.
+
+**Attributes**:
+
+- `source_locale` - Optional BCP 47 language code representing the source
+ language. When omitted, the API attempts automatic detection.
+- `target_locale` - Required BCP 47 language code for the desired
+ translation target.
+- `fast` - Optional flag that enables the service's low-latency translation
+ mode at the cost of some quality safeguards.
+- `reference` - Optional nested mapping of existing translations that
+ provides additional context to the engine.
+
+
+
+### validate\_locale
+
+```python
+@validator("source_locale", "target_locale")
+@classmethod
+def validate_locale(cls, v: Optional[str]) -> Optional[str]
+```
+
+Validates that locale codes conform to BCP 47 standards.
+
+**Arguments**:
+
+- `v` - The locale string to validate, or None.
+
+
+**Returns**:
+
+ The validated locale string or None.
+
+
+**Raises**:
+
+- `ValueError` - If the locale is not a valid BCP 47 language tag.
+
+
+
+## LingoDotDevEngine Objects
+
+```python
+class LingoDotDevEngine()
+```
+
+Asynchronous client for the Lingo.dev localization API.
+
+The engine manages an httpx.AsyncClient, handles chunking and batching of
+content, and exposes helper coroutines for translating strings, structured
+objects, and chat transcripts.
+
+All localization methods are async and must be awaited.
+
+
+
+### \_\_init\_\_
+
+```python
+def __init__(config: Dict[str, Any])
+```
+
+Instantiates the engine with configuration data.
+
+**Arguments**:
+
+- `config` - Mapping of values understood by `EngineConfig`. At minimum
+ an `api_key` entry must be supplied.
+
+
+**Raises**:
+
+- `ValueError` - If the supplied configuration fails validation.
+
+
+
+### \_\_aenter\_\_
+
+```python
+async def __aenter__()
+```
+
+Opens the HTTP session when entering an async context.
+
+**Returns**:
+
+- ``LingoDotDevEngine`` - The active engine instance.
+
+
+
+### \_\_aexit\_\_
+
+```python
+async def __aexit__(exc_type, exc_val, exc_tb)
+```
+
+Releases resources acquired during the async context.
+
+
+
+### close
+
+```python
+async def close()
+```
+
+Closes the HTTP client if it has been created.
+
+
+
+### localize\_object
+
+```python
+async def localize_object(obj: Dict[str, Any],
+ params: Dict[str, Any],
+ progress_callback: Optional[
+ Callable[[int, Dict[str, str], Dict[str, str]],
+ None]] = None,
+ concurrent: bool = False) -> Dict[str, Any]
+```
+
+Localizes every string value contained in a mapping.
+
+**Arguments**:
+
+- `obj` - Mapping whose string leaves should be translated.
+- `params` - Dictionary of options accepted by `LocalizationParams`.
+- `progress_callback` - Optional callable invoked with progress updates
+ (0-100) alongside the source and localized chunks. Do not set
+ `concurrent` when providing this callback because progress
+ updates are unavailable in concurrent mode.
+- `concurrent` - When `True` the payload chunks are processed
+ concurrently and no progress updates are emitted.
+
+
+**Returns**:
+
+ A dictionary mirroring `obj` with localized string values.
+
+
+**Raises**:
+
+- `RuntimeError` - If the API responds with an error.
+- `ValueError` - If the API rejects the request.
+
+
+**Examples**:
+
+ ```python
+ async with LingoDotDevEngine({"api_key": "token"}) as engine:
+ localized = await engine.localize_object(
+ {"title": "Hello"},
+ {"target_locale": "es"},
+ concurrent=True,
+ )
+ # localized -> {"title": "Hola"} (example output)
+
+ async with LingoDotDevEngine({"api_key": "token"}) as engine:
+ def on_progress(percent, *_):
+ print(f"Progress: {percent}%")
+
+ localized = await engine.localize_object(
+ {"welcome": "Hello", "farewell": "Goodbye"},
+ {"source_locale": "en", "target_locale": "fr"},
+ progress_callback=on_progress,
+ )
+ # localized -> {"welcome": "Bonjour", "farewell": "Au revoir"} (example output)
+ ```
+
+
+
+### localize\_text
+
+```python
+async def localize_text(
+ text: str,
+ params: Dict[str, Any],
+ progress_callback: Optional[Callable[[int], None]] = None) -> str
+```
+
+Localizes a single text string.
+
+**Arguments**:
+
+- `text` - The text to translate.
+- `params` - Dictionary of options accepted by `LocalizationParams`.
+- `progress_callback` - Optional callable receiving the percentage
+ complete (0-100).
+
+
+**Returns**:
+
+ The localized text string.
+
+
+**Raises**:
+
+- `RuntimeError` - If the API responds with an error.
+- `ValueError` - If the API rejects the request.
+
+
+**Examples**:
+
+ ```python
+ async with LingoDotDevEngine({"api_key": "token"}) as engine:
+ greeting = await engine.localize_text(
+ "Hello", {"target_locale": "de"}
+ )
+ # greeting -> "Hallo" (example output)
+
+ progress_updates = []
+
+ async with LingoDotDevEngine({"api_key": "token"}) as engine:
+ farewell = await engine.localize_text(
+ "Goodbye",
+ {"source_locale": "en", "target_locale": "it"},
+ progress_callback=progress_updates.append,
+ )
+ # farewell -> "Arrivederci" (example output)
+ ```
+
+
+
+### batch\_localize\_text
+
+```python
+async def batch_localize_text(text: str, params: Dict[str, Any]) -> List[str]
+```
+
+Localizes a single text string into multiple target locales.
+
+**Arguments**:
+
+- `text` - The text string to translate.
+- `params` - Dictionary of options accepted by `LocalizationParams` plus
+ a `target_locales` list.
+
+
+**Returns**:
+
+ Localized strings ordered to match `target_locales`.
+
+
+**Raises**:
+
+- `ValueError` - If `target_locales` is missing or the API rejects a
+ request.
+- `RuntimeError` - If the API responds with an error.
+
+
+**Examples**:
+
+ ```python
+ async with LingoDotDevEngine({"api_key": "token"}) as engine:
+ variants = await engine.batch_localize_text(
+ "Welcome",
+ {"target_locales": ["es", "fr"]},
+ )
+ # variants -> ["Bienvenido", "Bienvenue"] (example output)
+
+ async with LingoDotDevEngine({"api_key": "token"}) as engine:
+ variants = await engine.batch_localize_text(
+ "Checkout",
+ {
+ "source_locale": "en",
+ "target_locales": ["pt-BR", "it"],
+ "fast": True,
+ },
+ )
+ # variants -> ["Finalizar compra", "Pagamento"] (example output)
+ ```
+
+
+
+### localize\_chat
+
+```python
+async def localize_chat(
+ chat: List[Dict[str, str]],
+ params: Dict[str, Any],
+ progress_callback: Optional[Callable[[int], None]] = None
+) -> List[Dict[str, str]]
+```
+
+Localizes a chat transcript while preserving speaker metadata.
+
+**Arguments**:
+
+- `chat` - Sequence of chat messages. Each item must include `name` and
+ `text` keys.
+- `params` - Dictionary of options accepted by `LocalizationParams`.
+- `progress_callback` - Optional callable receiving percentage updates
+ (0-100) while the transcript is localized.
+
+
+**Returns**:
+
+ Localized chat messages in the same order as `chat`. If the API
+ omits chat data an empty list is returned.
+
+
+**Raises**:
+
+- `ValueError` - If any message in `chat` omits the required keys.
+- `RuntimeError` - If the API responds with an error.
+
+
+**Examples**:
+
+ ```python
+ chat = [
+ {"name": "Alice", "text": "Hello"},
+ {"name": "Bob", "text": "Goodbye"},
+ ]
+
+ async with LingoDotDevEngine({"api_key": "token"}) as engine:
+ translated = await engine.localize_chat(
+ chat,
+ {"target_locale": "es"},
+ )
+ # translated -> [{"name": "Alice", "text": "Hola"}, ...] (example output)
+
+ updates = []
+
+ async with LingoDotDevEngine({"api_key": "token"}) as engine:
+ translated = await engine.localize_chat(
+ chat,
+ {"source_locale": "en", "target_locale": "de"},
+ progress_callback=updates.append,
+ )
+ # translated -> [{"name": "Alice", "text": "Hallo"}, ...] (example output)
+ ```
+
+
+
+### recognize\_locale
+
+```python
+async def recognize_locale(text: str) -> str
+```
+
+Detects the language of the supplied text.
+
+**Arguments**:
+
+- `text` - Non-empty string to analyse.
+
+
+**Returns**:
+
+ Locale code reported by the API (for example `"en"`) or an
+ empty string when the service cannot determine a locale.
+
+
+**Raises**:
+
+- `ValueError` - If `text` is empty or only whitespace.
+- `RuntimeError` - If the request fails or the API reports an error.
+
+
+
+### whoami
+
+```python
+async def whoami() -> Optional[Dict[str, str]]
+```
+
+Retrieves account metadata associated with the current API key.
+
+**Returns**:
+
+ Dictionary containing `email` and `id` keys when available, or
+ `None` if the key is unauthenticated or a recoverable network error
+ occurs.
+
+
+**Raises**:
+
+- `RuntimeError` - If the service reports a server-side error.
+
+
+
+### batch\_localize\_objects
+
+```python
+async def batch_localize_objects(
+ objects: List[Dict[str, Any]],
+ params: Dict[str, Any]) -> List[Dict[str, Any]]
+```
+
+Localizes multiple mapping objects concurrently.
+
+**Arguments**:
+
+- `objects` - List of objects whose string values should be translated.
+- `params` - Dictionary of options accepted by `LocalizationParams`,
+ shared across all objects.
+
+
+**Returns**:
+
+ Localized objects preserving the order of `objects`.
+
+
+**Raises**:
+
+- `RuntimeError` - If the API responds with an error.
+- `ValueError` - If the API rejects a request.
+
+
+
+### quick\_translate
+
+```python
+@classmethod
+async def quick_translate(cls,
+ content: Any,
+ api_key: str,
+ target_locale: str,
+ source_locale: Optional[str] = None,
+ api_url: str = "https://engine.lingo.dev",
+ fast: bool = True) -> Any
+```
+
+Translates content without managing an engine instance manually.
+
+The helper opens a `LingoDotDevEngine` using the supplied configuration,
+performs the translation, and automatically closes the underlying HTTP
+client.
+
+**Arguments**:
+
+- `content` - Text string or mapping to translate. The returned value
+ matches the type of `content`.
+- `api_key` - Lingo.dev API key to authenticate the request.
+- `target_locale` - Target language code for the translation.
+- `source_locale` - Optional source language code. When omitted the API
+ may attempt to detect it.
+- `api_url` - Lingo.dev engine base URL. Defaults to
+ `"https://engine.lingo.dev"`.
+- `fast` - Whether to enable the service's fast translation mode.
+
+
+**Returns**:
+
+ Translated content with the same type as `content`.
+
+
+**Raises**:
+
+- `ValueError` - If `content` is not a string or dictionary, or if the
+ service rejects the request.
+- `RuntimeError` - If the API indicates a failure or the request cannot
+ be completed.
+
+
+**Examples**:
+
+ ```python
+ greeting = await LingoDotDevEngine.quick_translate(
+ "Hello world",
+ api_key="api-key",
+ target_locale="es",
+ )
+ # greeting -> "Hola mundo" (example output)
+
+ landing_page = await LingoDotDevEngine.quick_translate(
+ {"headline": "Hello", "cta": "Buy now"},
+ api_key="api-key",
+ target_locale="de",
+ source_locale="en",
+ fast=False,
+ )
+ # landing_page -> {"headline": "Hallo", "cta": "Jetzt kaufen"} (example output)
+ ```
+
+
+
+### quick\_batch\_translate
+
+```python
+@classmethod
+async def quick_batch_translate(cls,
+ content: Any,
+ api_key: str,
+ target_locales: List[str],
+ source_locale: Optional[str] = None,
+ api_url: str = "https://engine.lingo.dev",
+ fast: bool = True) -> List[Any]
+```
+
+Translates content into multiple locales without manual setup.
+
+**Arguments**:
+
+- `content` - Text string or mapping to translate for each locale.
+- `api_key` - Lingo.dev API key to authenticate requests.
+- `target_locales` - List of locale codes. Results maintain this order.
+- `source_locale` - Optional source language code. When omitted the API
+ may attempt to detect it.
+- `api_url` - Lingo.dev engine base URL. Defaults to
+ `"https://engine.lingo.dev"`.
+- `fast` - Whether to enable the service's fast translation mode.
+
+
+**Returns**:
+
+ Translated content, one entry per `target_locales` item.
+
+
+**Raises**:
+
+- `ValueError` - If `content` is not a string or dictionary, or if a
+ request is rejected by the API.
+- `RuntimeError` - If the API indicates a failure or the request cannot
+ be completed.
+
+
+**Examples**:
+
+ ```python
+ variants = await LingoDotDevEngine.quick_batch_translate(
+ "Hello world",
+ api_key="api-key",
+ target_locales=["es", "fr"],
+ )
+ # variants -> ["Hola mundo", "Bonjour le monde"] (example output)
+
+ localized_objects = await LingoDotDevEngine.quick_batch_translate(
+ {"success": "Saved", "error": "Failed"},
+ api_key="api-key",
+ target_locales=["pt-BR", "it"],
+ source_locale="en",
+ fast=False,
+ )
+ # localized_objects -> [
+ # {"success": "Salvo", "error": "Falhou"},
+ # {"success": "Salvato", "error": "Non riuscito"},
+ # ] (example output)
+ ```
+
diff --git a/pydoc-markdown.yml b/pydoc-markdown.yml
new file mode 100644
index 0000000..f042ad0
--- /dev/null
+++ b/pydoc-markdown.yml
@@ -0,0 +1,21 @@
+loaders:
+ - type: python
+ search_path:
+ - src
+ packages:
+ - lingodotdev
+processors:
+ - type: filter
+ skip_empty_modules: true
+ - type: smart
+ - type: crossref
+renderer:
+ type: markdown
+ filename: docs/api.md
+ render_toc: true
+ header_level_by_type:
+ Module: 1
+ Class: 2
+ Method: 3
+ Function: 3
+ Variable: 3
diff --git a/pyproject.toml b/pyproject.toml
index 0f3eb26..a462756 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,7 +7,7 @@ name = "lingodotdev"
version = "1.3.0"
description = "Lingo.dev Python SDK"
readme = "README.md"
-requires-python = ">=3.8"
+requires-python = ">=3.9"
license = { text = "Apache-2.0" }
authors = [
{ name = "Lingo.dev Team", email = "hi@lingo.dev" },
@@ -20,11 +20,11 @@ classifiers = [
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Software Development :: Localization",
"Topic :: Text Processing :: Linguistic",
@@ -44,6 +44,7 @@ dev = [
"flake8>=6.0.0",
"mypy>=1.0.0",
"python-semantic-release>=8.0.0",
+ "pydoc-markdown>=4.8",
]
[project.urls]
@@ -129,4 +130,4 @@ changelog_file = "CHANGELOG.md"
[tool.semantic_release.branches.main]
match = "main"
-prerelease = false
\ No newline at end of file
+prerelease = false
diff --git a/src/lingodotdev/__init__.py b/src/lingodotdev/__init__.py
index ce0d641..9f677ae 100644
--- a/src/lingodotdev/__init__.py
+++ b/src/lingodotdev/__init__.py
@@ -1,8 +1,11 @@
-"""
-Lingo.dev Python SDK
+"""Public entry point for the Lingo.dev Python SDK.
+
+The package exposes LingoDotDevEngine, the asynchronous client used to access
+the Lingo.dev localization API. Refer to the engine module for detailed usage
+guidance.
-A powerful localization engine that supports various content types including
-plain text, objects, chat sequences, and HTML documents.
+ async with LingoDotDevEngine({"api_key": "..."}) as engine:
+ result = await engine.localize_text("Hello", {"target_locale": "es"})
"""
__version__ = "1.3.0"
diff --git a/src/lingodotdev/engine.py b/src/lingodotdev/engine.py
index 293bead..efcc8f2 100644
--- a/src/lingodotdev/engine.py
+++ b/src/lingodotdev/engine.py
@@ -1,10 +1,17 @@
-"""
-LingoDotDevEngine implementation for Python SDK - Async version with httpx
+"""Async client implementation for the Lingo.dev localization service.
+
+This module provides LingoDotDevEngine and supporting data models for
+configuration and localization parameter validation.
+
+ config = {"api_key": "your-api-key"}
+ async with LingoDotDevEngine(config) as engine:
+ result = await engine.localize_text("Hello", {"target_locale": "es"})
"""
# mypy: disable-error-code=unreachable
import asyncio
+import re
from typing import Any, Callable, Dict, List, Optional
from urllib.parse import urljoin
@@ -12,9 +19,24 @@
from nanoid import generate
from pydantic import BaseModel, Field, validator
+_BCP47_TAG_RE = re.compile(r"^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$")
+
class EngineConfig(BaseModel):
- """Configuration for the LingoDotDevEngine"""
+ """Runtime configuration for LingoDotDevEngine.
+
+ Stores and validates configuration parameters required to interact with the
+ Lingo.dev API.
+
+ Attributes:
+ api_key: Secret token used to authenticate with the Lingo.dev API.
+ api_url: Base endpoint for the localization engine. Defaults to
+ 'https://engine.lingo.dev'.
+ batch_size: Maximum number of top-level entries to send in a single
+ localization request (between 1 and 250 inclusive).
+ ideal_batch_item_size: Target word count per request before payloads are
+ split into multiple batches (between 1 and 2500 inclusive).
+ """
api_key: str
api_url: str = "https://engine.lingo.dev"
@@ -24,48 +46,104 @@ class EngineConfig(BaseModel):
@validator("api_url")
@classmethod
def validate_api_url(cls, v: str) -> str:
+ """Validates that the API URL is a valid HTTP/HTTPS URL.
+
+ Args:
+ v: The URL string to validate.
+
+ Returns:
+ The validated URL string.
+
+ Raises:
+ ValueError: If the URL doesn't start with http:// or https://.
+ """
if not v.startswith(("http://", "https://")):
raise ValueError("API URL must be a valid HTTP/HTTPS URL")
return v
class LocalizationParams(BaseModel):
- """Parameters for localization requests"""
+ """Request parameters for localization operations.
+
+ These values are serialized directly into API requests after validation.
+
+ Attributes:
+ source_locale: Optional BCP 47 language code representing the source
+ language. When omitted, the API attempts automatic detection.
+ target_locale: Required BCP 47 language code for the desired
+ translation target.
+ fast: Optional flag that enables the service's low-latency translation
+ mode at the cost of some quality safeguards.
+ reference: Optional nested mapping of existing translations that
+ provides additional context to the engine.
+ """
source_locale: Optional[str] = None
target_locale: str
fast: Optional[bool] = None
reference: Optional[Dict[str, Dict[str, Any]]] = None
+ @validator("source_locale", "target_locale")
+ @classmethod
+ def validate_locale(cls, v: Optional[str]) -> Optional[str]:
+ """Validates that locale codes conform to BCP 47 standards.
+
+ Args:
+ v: The locale string to validate, or None.
+
+ Returns:
+ The validated locale string or None.
+
+ Raises:
+ ValueError: If the locale is not a valid BCP 47 language tag.
+ """
+ if v is None:
+ return v
+ if not _BCP47_TAG_RE.fullmatch(v):
+ raise ValueError(
+ "Locale values must be valid BCP 47 language tags (example: 'en', 'en-US')."
+ )
+ return v
+
class LingoDotDevEngine:
- """
- LingoDotDevEngine class for interacting with the LingoDotDev API
- A powerful localization engine that supports various content types including
- plain text, objects, chat sequences, and HTML documents.
+ """Asynchronous client for the Lingo.dev localization API.
+
+ The engine manages an httpx.AsyncClient, handles chunking and batching of
+ content, and exposes helper coroutines for translating strings, structured
+ objects, and chat transcripts.
+
+ All localization methods are async and must be awaited.
"""
def __init__(self, config: Dict[str, Any]):
- """
- Create a new LingoDotDevEngine instance
+ """Instantiates the engine with configuration data.
Args:
- config: Configuration options for the Engine
+ config: Mapping of values understood by `EngineConfig`. At minimum
+ an `api_key` entry must be supplied.
+
+ Raises:
+ ValueError: If the supplied configuration fails validation.
"""
self.config = EngineConfig(**config)
self._client: Optional[httpx.AsyncClient] = None
async def __aenter__(self):
- """Async context manager entry"""
+ """Opens the HTTP session when entering an async context.
+
+ Returns:
+ `LingoDotDevEngine`: The active engine instance.
+ """
await self._ensure_client()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
- """Async context manager exit"""
+ """Releases resources acquired during the async context."""
await self.close()
async def _ensure_client(self):
- """Ensure the httpx client is initialized"""
+ """Creates an `httpx.AsyncClient` if one is not already available."""
if self._client is None or self._client.is_closed:
self._client = httpx.AsyncClient(
headers={
@@ -76,7 +154,7 @@ async def _ensure_client(self):
)
async def close(self):
- """Close the httpx client"""
+ """Closes the HTTP client if it has been created."""
if self._client and not self._client.is_closed:
await self._client.aclose()
@@ -89,17 +167,28 @@ async def _localize_raw(
] = None,
concurrent: bool = False,
) -> Dict[str, str]:
- """
- Localize content using the Lingo.dev API
+ """Submits a localization request for the provided payload.
+
+ The payload is split into chunks based on the configured limits and the
+ resulting pieces are localized sequentially or concurrently. Sequential
+ mode enables progress reporting while concurrent mode maximizes
+ throughput.
Args:
- payload: The content to be localized
- params: Localization parameters
- progress_callback: Optional callback function to report progress (0-100)
- concurrent: Whether to process chunks concurrently (faster but no progress tracking)
+ payload: Mapping or structured content to be localized.
+ params: Pre-validated localization parameters.
+ progress_callback: Optional callable invoked after each chunk is
+ localized. Receives the percentage completed, the source chunk,
+ and the localized chunk.
+ concurrent: When `True` and no `progress_callback` is supplied,
+ chunks are processed concurrently.
Returns:
- Localized content
+ Dictionary containing the merged localized chunks.
+
+ Raises:
+ RuntimeError: If the API responds with an error.
+ ValueError: If the API rejects the payload.
"""
await self._ensure_client()
chunked_payload = self._extract_payload_chunks(payload)
@@ -154,18 +243,24 @@ async def _localize_chunk(
workflow_id: str,
fast: bool,
) -> Dict[str, str]:
- """
- Localize a single chunk of content
+ """Translates a single payload chunk through the `/i18n` endpoint.
Args:
- 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
+ source_locale: Optional source locale used for the request.
+ target_locale: Target locale requested from the API.
+ payload: Dictionary containing the chunk under the `data` key and
+ optional `reference` metadata.
+ workflow_id: Identifier shared across chunks that belong to the
+ same localization workflow.
+ fast: Whether to request the service's fast translation mode.
Returns:
- Localized chunk
+ Localized chunk returned by the API.
+
+ Raises:
+ RuntimeError: If the API responds with an error status or signals a
+ streaming error.
+ ValueError: If the API reports an invalid request (HTTP 400).
"""
await self._ensure_client()
assert self._client is not None # Type guard for mypy
@@ -208,14 +303,17 @@ async def _localize_chunk(
raise RuntimeError(f"Request failed: {str(e)}")
def _extract_payload_chunks(self, payload: Dict[str, Any]) -> List[Dict[str, Any]]:
- """
- Extract payload chunks based on the ideal chunk size
+ """Splits a payload into smaller dictionaries based on configured limits.
+
+ The method iterates through the payload in insertion order, grouping
+ keys until the number of words or items would exceed the configured
+ thresholds. Each chunk is suitable for sending directly to the API.
Args:
- payload: The payload to be chunked
+ payload: Mapping to be divided into localization chunks.
Returns:
- An array of payload chunks
+ Individual request chunks ready for the API.
"""
result = []
current_chunk = {}
@@ -240,14 +338,13 @@ def _extract_payload_chunks(self, payload: Dict[str, Any]) -> List[Dict[str, Any
return result
def _count_words_in_record(self, payload: Any) -> int:
- """
- Count words in a record or array
+ """Recursively counts whitespace-delimited words within a payload.
Args:
- payload: The payload to count words in
+ payload: String, mapping, list, or other primitive values to count.
Returns:
- The total number of words
+ Total number of words discovered within string values.
"""
if isinstance(payload, list):
return sum(self._count_words_in_record(item) for item in payload)
@@ -267,20 +364,46 @@ async def localize_object(
] = None,
concurrent: bool = False,
) -> Dict[str, Any]:
- """
- Localize a typical Python dictionary
+ """Localizes every string value contained in a mapping.
Args:
- obj: The object to be localized (strings will be extracted and translated)
- params: Localization parameters:
- - source_locale: The source language code (e.g., 'en')
- - target_locale: The target language code (e.g., 'es')
- - fast: Optional boolean to enable fast mode
- progress_callback: Optional callback function to report progress (0-100)
- concurrent: Whether to process chunks concurrently (faster but no progress tracking)
+ obj: Mapping whose string leaves should be translated.
+ params: Dictionary of options accepted by `LocalizationParams`.
+ progress_callback: Optional callable invoked with progress updates
+ (0-100) alongside the source and localized chunks. Do not set
+ `concurrent` when providing this callback because progress
+ updates are unavailable in concurrent mode.
+ concurrent: When `True` the payload chunks are processed
+ concurrently and no progress updates are emitted.
Returns:
- A new object with the same structure but localized string values
+ A dictionary mirroring `obj` with localized string values.
+
+ Raises:
+ RuntimeError: If the API responds with an error.
+ ValueError: If the API rejects the request.
+
+ Examples:
+ ```python
+ async with LingoDotDevEngine({"api_key": "token"}) as engine:
+ localized = await engine.localize_object(
+ {"title": "Hello"},
+ {"target_locale": "es"},
+ concurrent=True,
+ )
+ # localized -> {"title": "Hola"} (example output)
+
+ async with LingoDotDevEngine({"api_key": "token"}) as engine:
+ def on_progress(percent, *_):
+ print(f"Progress: {percent}%")
+
+ localized = await engine.localize_object(
+ {"welcome": "Hello", "farewell": "Goodbye"},
+ {"source_locale": "en", "target_locale": "fr"},
+ progress_callback=on_progress,
+ )
+ # localized -> {"welcome": "Bonjour", "farewell": "Au revoir"} (example output)
+ ```
"""
localization_params = LocalizationParams(**params)
return await self._localize_raw(
@@ -293,19 +416,39 @@ async def localize_text(
params: Dict[str, Any],
progress_callback: Optional[Callable[[int], None]] = None,
) -> str:
- """
- Localize a single text string
+ """Localizes a single text string.
Args:
- text: The text string to be localized
- params: Localization parameters:
- - source_locale: The source language code (e.g., 'en')
- - target_locale: The target language code (e.g., 'es')
- - fast: Optional boolean to enable fast mode
- progress_callback: Optional callback function to report progress (0-100)
+ text: The text to translate.
+ params: Dictionary of options accepted by `LocalizationParams`.
+ progress_callback: Optional callable receiving the percentage
+ complete (0-100).
Returns:
- The localized text string
+ The localized text string.
+
+ Raises:
+ RuntimeError: If the API responds with an error.
+ ValueError: If the API rejects the request.
+
+ Examples:
+ ```python
+ async with LingoDotDevEngine({"api_key": "token"}) as engine:
+ greeting = await engine.localize_text(
+ "Hello", {"target_locale": "de"}
+ )
+ # greeting -> "Hallo" (example output)
+
+ progress_updates = []
+
+ async with LingoDotDevEngine({"api_key": "token"}) as engine:
+ farewell = await engine.localize_text(
+ "Goodbye",
+ {"source_locale": "en", "target_locale": "it"},
+ progress_callback=progress_updates.append,
+ )
+ # farewell -> "Arrivederci" (example output)
+ ```
"""
localization_params = LocalizationParams(**params)
@@ -322,18 +465,41 @@ def wrapped_progress_callback(
return response.get("text", "")
async def batch_localize_text(self, text: str, params: Dict[str, Any]) -> List[str]:
- """
- Localize a text string to multiple target locales
+ """Localizes a single text string into multiple target locales.
Args:
- text: The text string to be localized
- params: Localization parameters:
- - source_locale: The source language code (e.g., 'en')
- - target_locales: A list of target language codes (e.g., ['es', 'fr'])
- - fast: Optional boolean to enable fast mode
+ text: The text string to translate.
+ params: Dictionary of options accepted by `LocalizationParams` plus
+ a `target_locales` list.
Returns:
- A list of localized text strings
+ Localized strings ordered to match `target_locales`.
+
+ Raises:
+ ValueError: If `target_locales` is missing or the API rejects a
+ request.
+ RuntimeError: If the API responds with an error.
+
+ Examples:
+ ```python
+ async with LingoDotDevEngine({"api_key": "token"}) as engine:
+ variants = await engine.batch_localize_text(
+ "Welcome",
+ {"target_locales": ["es", "fr"]},
+ )
+ # variants -> ["Bienvenido", "Bienvenue"] (example output)
+
+ async with LingoDotDevEngine({"api_key": "token"}) as engine:
+ variants = await engine.batch_localize_text(
+ "Checkout",
+ {
+ "source_locale": "en",
+ "target_locales": ["pt-BR", "it"],
+ "fast": True,
+ },
+ )
+ # variants -> ["Finalizar compra", "Pagamento"] (example output)
+ ```
"""
if "target_locales" not in params:
raise ValueError("target_locales is required")
@@ -365,19 +531,47 @@ async def localize_chat(
params: Dict[str, Any],
progress_callback: Optional[Callable[[int], None]] = None,
) -> List[Dict[str, str]]:
- """
- Localize a chat sequence while preserving speaker names
+ """Localizes a chat transcript while preserving speaker metadata.
Args:
- chat: Array of chat messages, each with 'name' and 'text' properties
- params: Localization parameters:
- - source_locale: The source language code (e.g., 'en')
- - target_locale: The target language code (e.g., 'es')
- - fast: Optional boolean to enable fast mode
- progress_callback: Optional callback function to report progress (0-100)
+ chat: Sequence of chat messages. Each item must include `name` and
+ `text` keys.
+ params: Dictionary of options accepted by `LocalizationParams`.
+ progress_callback: Optional callable receiving percentage updates
+ (0-100) while the transcript is localized.
Returns:
- Array of localized chat messages with preserved structure
+ Localized chat messages in the same order as `chat`. If the API
+ omits chat data an empty list is returned.
+
+ Raises:
+ ValueError: If any message in `chat` omits the required keys.
+ RuntimeError: If the API responds with an error.
+
+ Examples:
+ ```python
+ chat = [
+ {"name": "Alice", "text": "Hello"},
+ {"name": "Bob", "text": "Goodbye"},
+ ]
+
+ async with LingoDotDevEngine({"api_key": "token"}) as engine:
+ translated = await engine.localize_chat(
+ chat,
+ {"target_locale": "es"},
+ )
+ # translated -> [{"name": "Alice", "text": "Hola"}, ...] (example output)
+
+ updates = []
+
+ async with LingoDotDevEngine({"api_key": "token"}) as engine:
+ translated = await engine.localize_chat(
+ chat,
+ {"source_locale": "en", "target_locale": "de"},
+ progress_callback=updates.append,
+ )
+ # translated -> [{"name": "Alice", "text": "Hallo"}, ...] (example output)
+ ```
"""
# Validate chat format
for message in chat:
@@ -406,14 +600,18 @@ def wrapped_progress_callback(
return []
async def recognize_locale(self, text: str) -> str:
- """
- Detect the language of a given text
+ """Detects the language of the supplied text.
Args:
- text: The text to analyze
+ text: Non-empty string to analyse.
Returns:
- A locale code (e.g., 'en', 'es', 'fr')
+ Locale code reported by the API (for example `"en"`) or an
+ empty string when the service cannot determine a locale.
+
+ Raises:
+ ValueError: If `text` is empty or only whitespace.
+ RuntimeError: If the request fails or the API reports an error.
"""
if not text or not text.strip():
raise ValueError("Text cannot be empty")
@@ -442,11 +640,15 @@ async def recognize_locale(self, text: str) -> str:
raise RuntimeError(f"Request failed: {str(e)}")
async def whoami(self) -> Optional[Dict[str, str]]:
- """
- Get information about the current API key
+ """Retrieves account metadata associated with the current API key.
Returns:
- Dictionary with 'email' and 'id' keys, or None if not authenticated
+ Dictionary containing `email` and `id` keys when available, or
+ `None` if the key is unauthenticated or a recoverable network error
+ occurs.
+
+ Raises:
+ RuntimeError: If the service reports a server-side error.
"""
await self._ensure_client()
assert self._client is not None # Type guard for mypy
@@ -477,15 +679,19 @@ async def whoami(self) -> Optional[Dict[str, str]]:
async def batch_localize_objects(
self, objects: List[Dict[str, Any]], params: Dict[str, Any]
) -> List[Dict[str, Any]]:
- """
- Localize multiple objects concurrently
+ """Localizes multiple mapping objects concurrently.
Args:
- objects: List of objects to localize
- params: Localization parameters
+ objects: List of objects whose string values should be translated.
+ params: Dictionary of options accepted by `LocalizationParams`,
+ shared across all objects.
Returns:
- List of localized objects
+ Localized objects preserving the order of `objects`.
+
+ Raises:
+ RuntimeError: If the API responds with an error.
+ ValueError: If the API rejects a request.
"""
tasks = []
for obj in objects:
@@ -504,35 +710,50 @@ async def quick_translate(
api_url: str = "https://engine.lingo.dev",
fast: bool = True,
) -> Any:
- """
- Quick one-off translation without manual context management.
- Automatically handles the async context manager.
+ """Translates content without managing an engine instance manually.
+
+ The helper opens a `LingoDotDevEngine` using the supplied configuration,
+ performs the translation, and automatically closes the underlying HTTP
+ client.
Args:
- content: Text string or dict to 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)
- api_url: API endpoint URL
- fast: Enable fast mode for quicker translations
+ content: Text string or mapping to translate. The returned value
+ matches the type of `content`.
+ api_key: Lingo.dev API key to authenticate the request.
+ target_locale: Target language code for the translation.
+ source_locale: Optional source language code. When omitted the API
+ may attempt to detect it.
+ api_url: Lingo.dev engine base URL. Defaults to
+ `"https://engine.lingo.dev"`.
+ fast: Whether to enable the service's fast translation mode.
Returns:
- Translated content (same type as input)
+ Translated content with the same type as `content`.
+
+ Raises:
+ ValueError: If `content` is not a string or dictionary, or if the
+ service rejects the request.
+ RuntimeError: If the API indicates a failure or the request cannot
+ be completed.
- Example:
- # Translate text
- result = await LingoDotDevEngine.quick_translate(
+ Examples:
+ ```python
+ greeting = await LingoDotDevEngine.quick_translate(
"Hello world",
- "your-api-key",
- "es"
+ api_key="api-key",
+ target_locale="es",
)
-
- # Translate object
- result = await LingoDotDevEngine.quick_translate(
- {"greeting": "Hello", "farewell": "Goodbye"},
- "your-api-key",
- "es"
+ # greeting -> "Hola mundo" (example output)
+
+ landing_page = await LingoDotDevEngine.quick_translate(
+ {"headline": "Hello", "cta": "Buy now"},
+ api_key="api-key",
+ target_locale="de",
+ source_locale="en",
+ fast=False,
)
+ # landing_page -> {"headline": "Hallo", "cta": "Jetzt kaufen"} (example output)
+ ```
"""
config = {
"api_key": api_key,
@@ -563,28 +784,48 @@ async def quick_batch_translate(
api_url: str = "https://engine.lingo.dev",
fast: bool = True,
) -> List[Any]:
- """
- Quick batch translation to multiple target locales.
- Automatically handles the async context manager.
+ """Translates content into multiple locales without manual setup.
Args:
- content: Text string or dict to 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)
- api_url: API endpoint URL
- fast: Enable fast mode for quicker translations
+ content: Text string or mapping to translate for each locale.
+ api_key: Lingo.dev API key to authenticate requests.
+ target_locales: List of locale codes. Results maintain this order.
+ source_locale: Optional source language code. When omitted the API
+ may attempt to detect it.
+ api_url: Lingo.dev engine base URL. Defaults to
+ `"https://engine.lingo.dev"`.
+ fast: Whether to enable the service's fast translation mode.
Returns:
- List of translated content (one for each target locale)
+ Translated content, one entry per `target_locales` item.
- Example:
- results = await LingoDotDevEngine.quick_batch_translate(
+ Raises:
+ ValueError: If `content` is not a string or dictionary, or if a
+ request is rejected by the API.
+ RuntimeError: If the API indicates a failure or the request cannot
+ be completed.
+
+ Examples:
+ ```python
+ variants = await LingoDotDevEngine.quick_batch_translate(
"Hello world",
- "your-api-key",
- ["es", "fr", "de"]
+ api_key="api-key",
+ target_locales=["es", "fr"],
+ )
+ # variants -> ["Hola mundo", "Bonjour le monde"] (example output)
+
+ localized_objects = await LingoDotDevEngine.quick_batch_translate(
+ {"success": "Saved", "error": "Failed"},
+ api_key="api-key",
+ target_locales=["pt-BR", "it"],
+ source_locale="en",
+ fast=False,
)
- # Results: ["Hola mundo", "Bonjour le monde", "Hallo Welt"]
+ # localized_objects -> [
+ # {"success": "Salvo", "error": "Falhou"},
+ # {"success": "Salvato", "error": "Non riuscito"},
+ # ] (example output)
+ ```
"""
config = {
"api_key": api_key,