Skip to content

Commit 9f58f69

Browse files
committed
feat: make engine_id optional and use single /process/localize endpoint
1 parent f1d3f4f commit 9f58f69

File tree

4 files changed

+73
-44
lines changed

4 files changed

+73
-44
lines changed

README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ A powerful async-first localization engine that supports various content types i
88
- 🔀 **Concurrent processing** for dramatically faster bulk translations
99
- 🎯 **Multiple content types**: text, objects, chat messages, and more
1010
- 🌐 **Auto-detection** of source languages
11-
-**Fast mode** for quick translations
1211
- 🔧 **Flexible configuration** with progress callbacks
1312
- 📦 **Context manager** support for proper resource management
1413

@@ -56,7 +55,7 @@ from lingodotdev import LingoDotDevEngine
5655
async def main():
5756
config = {
5857
"api_key": "your-api-key",
59-
"engine_id": "your-engine-id",
58+
"engine_id": "your-engine-id", # Optional
6059
}
6160

6261
async with LingoDotDevEngine(config) as engine:
@@ -164,7 +163,7 @@ async def detection_example():
164163
```python
165164
config = {
166165
"api_key": "your-api-key", # Required: Your API key
167-
"engine_id": "your-engine-id", # Required: Your engine ID
166+
"engine_id": "your-engine-id", # Optional: Your engine ID
168167
"api_url": "https://api.lingo.dev", # Optional: API endpoint
169168
"batch_size": 25, # Optional: Items per batch (1-250)
170169
"ideal_batch_item_size": 250 # Optional: Target words per batch (1-2500)
@@ -176,7 +175,6 @@ config = {
176175
### Translation Parameters
177176
- **source_locale**: Source language code (auto-detected if None)
178177
- **target_locale**: Target language code (required)
179-
- **fast**: Enable fast mode for quicker translations
180178
- **reference**: Reference translations for context
181179
- **concurrent**: Process chunks concurrently (faster, but no progress callbacks)
182180

src/lingodotdev/engine.py

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,20 @@ class EngineConfig(BaseModel):
1717
"""Configuration for the LingoDotDevEngine"""
1818

1919
api_key: str
20-
engine_id: str
20+
engine_id: Optional[str] = None
2121
api_url: str = "https://api.lingo.dev"
2222
batch_size: int = Field(default=25, ge=1, le=250)
2323
ideal_batch_item_size: int = Field(default=250, ge=1, le=2500)
2424

25-
@validator("engine_id")
25+
@validator("engine_id", pre=True, always=True)
2626
@classmethod
27-
def validate_engine_id(cls, v: str) -> str:
28-
if not v.strip():
29-
raise ValueError("engine_id is required and cannot be empty")
30-
return v.strip()
27+
def validate_engine_id(cls, v: Optional[str]) -> Optional[str]:
28+
if v is None:
29+
return None
30+
stripped = v.strip()
31+
if not stripped:
32+
return None
33+
return stripped
3134

3235
@validator("api_url")
3336
@classmethod
@@ -204,14 +207,16 @@ async def _localize_chunk(
204207
await self._ensure_client()
205208
assert self._client is not None # Type guard for mypy
206209

207-
url = f"{self.config.api_url}/process/{self.config.engine_id}/localize"
210+
url = f"{self.config.api_url}/process/localize"
208211
request_data: Dict[str, Any] = {
209212
"params": {"fast": fast},
210213
"sourceLocale": source_locale,
211214
"targetLocale": target_locale,
212215
"data": payload["data"],
213216
"sessionId": self._session_id,
214217
}
218+
if self.config.engine_id:
219+
request_data["engineId"] = self.config.engine_id
215220
if payload.get("reference"):
216221
request_data["reference"] = payload["reference"]
217222

@@ -543,9 +548,9 @@ async def quick_translate(
543548
cls,
544549
content: Any,
545550
api_key: str,
546-
engine_id: str,
547551
target_locale: str,
548552
source_locale: Optional[str] = None,
553+
engine_id: Optional[str] = None,
549554
api_url: str = "https://api.lingo.dev",
550555
fast: bool = True,
551556
) -> Any:
@@ -556,9 +561,9 @@ async def quick_translate(
556561
Args:
557562
content: Text string or dict to translate
558563
api_key: Your Lingo.dev API key
559-
engine_id: Engine ID for the API
560564
target_locale: Target language code (e.g., 'es', 'fr')
561565
source_locale: Source language code (optional, auto-detected if None)
566+
engine_id: Optional engine ID for the API
562567
api_url: API endpoint URL
563568
fast: Enable fast mode for quicker translations
564569
@@ -582,11 +587,12 @@ async def quick_translate(
582587
"es"
583588
)
584589
"""
585-
config = {
590+
config: Dict[str, Any] = {
586591
"api_key": api_key,
587-
"engine_id": engine_id,
588592
"api_url": api_url,
589593
}
594+
if engine_id:
595+
config["engine_id"] = engine_id
590596

591597
async with cls(config) as engine:
592598
params = {
@@ -607,9 +613,9 @@ async def quick_batch_translate(
607613
cls,
608614
content: Any,
609615
api_key: str,
610-
engine_id: str,
611616
target_locales: List[str],
612617
source_locale: Optional[str] = None,
618+
engine_id: Optional[str] = None,
613619
api_url: str = "https://api.lingo.dev",
614620
fast: bool = True,
615621
) -> List[Any]:
@@ -620,9 +626,9 @@ async def quick_batch_translate(
620626
Args:
621627
content: Text string or dict to translate
622628
api_key: Your Lingo.dev API key
623-
engine_id: Engine ID for the API
624629
target_locales: List of target language codes (e.g., ['es', 'fr', 'de'])
625630
source_locale: Source language code (optional, auto-detected if None)
631+
engine_id: Optional engine ID for the API
626632
api_url: API endpoint URL
627633
fast: Enable fast mode for quicker translations
628634
@@ -633,16 +639,16 @@ async def quick_batch_translate(
633639
results = await LingoDotDevEngine.quick_batch_translate(
634640
"Hello world",
635641
"your-api-key",
636-
"your-engine-id",
637642
["es", "fr", "de"]
638643
)
639644
# Results: ["Hola mundo", "Bonjour le monde", "Hallo Welt"]
640645
"""
641-
config = {
646+
config: Dict[str, Any] = {
642647
"api_key": api_key,
643-
"engine_id": engine_id,
644648
"api_url": api_url,
645649
}
650+
if engine_id:
651+
config["engine_id"] = engine_id
646652

647653
async with cls(config) as engine:
648654
if isinstance(content, str):

tests/test_engine.py

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,20 +37,20 @@ def test_default_values(self):
3737
assert config.batch_size == 25
3838
assert config.ideal_batch_item_size == 250
3939

40-
def test_engine_id_required(self):
41-
"""Test that engine_id is required"""
42-
with pytest.raises(Exception, match="engine_id"):
43-
EngineConfig(api_key="test_key")
40+
def test_engine_id_optional(self):
41+
"""Test that engine_id is optional and defaults to None"""
42+
config = EngineConfig(api_key="test_key")
43+
assert config.engine_id is None
4444

45-
def test_engine_id_empty_string_rejected(self):
46-
"""Test that empty engine_id is rejected"""
47-
with pytest.raises(ValueError, match="engine_id is required"):
48-
EngineConfig(api_key="test_key", engine_id="")
45+
def test_engine_id_empty_string_becomes_none(self):
46+
"""Test that empty engine_id becomes None"""
47+
config = EngineConfig(api_key="test_key", engine_id="")
48+
assert config.engine_id is None
4949

50-
def test_engine_id_whitespace_rejected(self):
51-
"""Test that whitespace-only engine_id is rejected"""
52-
with pytest.raises(ValueError, match="engine_id is required"):
53-
EngineConfig(api_key="test_key", engine_id=" ")
50+
def test_engine_id_whitespace_becomes_none(self):
51+
"""Test that whitespace-only engine_id becomes None"""
52+
config = EngineConfig(api_key="test_key", engine_id=" ")
53+
assert config.engine_id is None
5454

5555
def test_engine_id_stripped(self):
5656
"""Test that engine_id is stripped of whitespace"""
@@ -702,14 +702,15 @@ async def test_localize_chunk_url_and_body(self, mock_post):
702702

703703
call_args = mock_post.call_args
704704
url = call_args[0][0]
705-
assert url == "https://api.lingo.dev/process/my-engine-id/localize"
705+
assert url == "https://api.lingo.dev/process/localize"
706706

707707
body = call_args[1]["json"]
708708
assert body["sourceLocale"] == "en"
709709
assert body["targetLocale"] == "es"
710710
assert body["params"] == {"fast": True}
711711
assert body["data"] == {"key": "value"}
712712
assert body["sessionId"] == self.engine._session_id
713+
assert body["engineId"] == "my-engine-id"
713714
assert body["reference"] == {"es": {"key": "ref"}}
714715

715716
@patch("lingodotdev.engine.httpx.AsyncClient.post")
@@ -756,9 +757,34 @@ async def test_full_localization_workflow(self, mock_post):
756757

757758
call_args = mock_post.call_args
758759
url = call_args[0][0]
759-
assert url == "https://api.lingo.dev/process/my-engine-id/localize"
760+
assert url == "https://api.lingo.dev/process/localize"
760761

761762
body = call_args[1]["json"]
762763
assert body["sourceLocale"] == "en"
763764
assert body["targetLocale"] == "es"
765+
assert body["engineId"] == "my-engine-id"
764766
assert "sessionId" in body
767+
768+
@patch("lingodotdev.engine.httpx.AsyncClient.post")
769+
async def test_localize_without_engine_id(self, mock_post):
770+
"""Test localization without engine_id omits engineId from body"""
771+
engine = LingoDotDevEngine({"api_key": "test_api_key"})
772+
773+
mock_response = Mock()
774+
mock_response.is_success = True
775+
mock_response.json.return_value = {"data": {"greeting": "hola"}}
776+
mock_post.return_value = mock_response
777+
778+
result = await engine.localize_object(
779+
{"greeting": "hello"},
780+
{"source_locale": "en", "target_locale": "es", "fast": True},
781+
)
782+
783+
assert result == {"greeting": "hola"}
784+
785+
call_args = mock_post.call_args
786+
url = call_args[0][0]
787+
assert url == "https://api.lingo.dev/process/localize"
788+
789+
body = call_args[1]["json"]
790+
assert "engineId" not in body

tests/test_integration.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,18 @@ class TestRealAPIIntegration:
3434
def setup_method(self):
3535
"""Set up test fixtures"""
3636
api_key = os.getenv("LINGODOTDEV_API_KEY")
37-
engine_id = os.getenv("LINGODOTDEV_ENGINE_ID")
3837
if not api_key:
3938
pytest.skip("No API key provided")
40-
if not engine_id:
41-
pytest.skip("No engine ID provided")
4239

43-
self.engine = LingoDotDevEngine(
44-
{
45-
"api_key": api_key,
46-
"engine_id": engine_id,
47-
"api_url": os.getenv("LINGODOTDEV_API_URL", "https://api.lingo.dev"),
48-
}
49-
)
40+
config = {
41+
"api_key": api_key,
42+
"api_url": os.getenv("LINGODOTDEV_API_URL", "https://api.lingo.dev"),
43+
}
44+
engine_id = os.getenv("LINGODOTDEV_ENGINE_ID")
45+
if engine_id:
46+
config["engine_id"] = engine_id
47+
48+
self.engine = LingoDotDevEngine(config)
5049

5150
async def test_localize_text_real_api(self):
5251
"""Test text localization against real API"""

0 commit comments

Comments
 (0)