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,