From 0533ab1b64981e7396c3188a8555c69cbb0b81e1 Mon Sep 17 00:00:00 2001 From: AndreyHirsa Date: Wed, 4 Mar 2026 15:22:02 +0300 Subject: [PATCH 1/2] feat: vNext migration --- README.md | 61 ++++-- composer.json | 3 +- src/LingoDotDevEngine.php | 118 ++++++------ test-all-methods.php | 65 +++++-- tests/LingoDotDevEngineTest.php | 328 ++++++++++++++++++++++++++++---- 5 files changed, 455 insertions(+), 120 deletions(-) diff --git a/README.md b/README.md index 3f99b34..e273028 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ composer require lingodotdev/sdk ## Basic Usage -After installing the package, bootstrap the engine with your API key: +After installing the package, bootstrap the engine with your API key and Engine ID: ```php require 'vendor/autoload.php'; @@ -20,10 +20,21 @@ require 'vendor/autoload.php'; use LingoDotDev\Sdk\LingoDotDevEngine; $engine = new LingoDotDevEngine([ - 'apiKey' => 'your-api-key', // replace with your actual key + 'apiKey' => 'your-api-key', // replace with your actual key + 'engineId' => 'your-engine-id', // replace with your actual engine id ]); ``` +### Configuration Options + +| Option | Type | Required | Default | Description | +|---|---|---|---|---| +| `apiKey` | string | Yes | — | Your Lingo.dev API key | +| `engineId` | string | Yes | — | Your Lingo.dev Engine ID | +| `apiUrl` | string | No | `https://api.lingo.dev` | API base URL | +| `batchSize` | int | No | `25` | Max items per chunk (1–250) | +| `idealBatchItemSize` | int | No | `250` | Max words per chunk (1–2500) | + ### Scenarios demonstrated in this README 1. Text Localization @@ -38,7 +49,6 @@ $engine = new LingoDotDevEngine([ - PHP 8.1 or higher - Composer - GuzzleHttp Client -- Respect Validation ## Getting Started @@ -74,9 +84,10 @@ Follow these steps to create a new PHP project that uses the Lingo.dev SDK: use LingoDotDev\Sdk\LingoDotDevEngine; -// Initialize the SDK with your API key +// Initialize the SDK with your API key and Engine ID $engine = new LingoDotDevEngine([ 'apiKey' => 'your-api-key', + 'engineId' => 'your-engine-id', ]); ``` @@ -93,6 +104,17 @@ $localizedText = $engine->localizeText('Hello, world!', [ // Output: "¡Hola, mundo!" ``` +You can enable fast mode for quicker (but potentially lower quality) translations: + +```php +// Localize with fast mode enabled +$localizedText = $engine->localizeText('Hello, world!', [ + 'sourceLocale' => 'en', + 'targetLocale' => 'es', + 'fast' => true, +]); +``` + ### Object Localization Translate an array of strings while preserving the structure: @@ -122,6 +144,21 @@ $localizedObject = $engine->localizeObject([ */ ``` +You can pass a reference to provide additional context: + +```php +// Localize with reference for additional context +$localizedObject = $engine->localizeObject([ + 'greeting' => 'Hello', +], [ + 'sourceLocale' => 'en', + 'targetLocale' => 'es', + 'reference' => [ + 'fr' => ['greeting' => 'Bonjour'] + ], +]); +``` + ### Chat Localization Translate a chat conversation while preserving speaker names: @@ -184,20 +221,20 @@ Track the progress of a localization operation: $engine->localizeText('Hello, world!', [ 'sourceLocale' => 'en', 'targetLocale' => 'es', -], function ($progress, $chunk, $processedChunk) { +], function ($progress) { echo "Localization progress: $progress%\n"; }); ``` ## Demo App -If you prefer to start with a minimal example instead of the detailed scenarios above, create **index.php** in an empty folder, copy the following snippet, install dependencies with `composer require lingodotdev/sdk`, set `LINGODOTDEV_API_KEY`, and run `php index.php`. +If you prefer to start with a minimal example instead of the detailed scenarios above, create **index.php** in an empty folder, copy the following snippet, install dependencies with `composer require lingodotdev/sdk`, set `LINGODOTDEV_API_KEY` and `LINGODOTDEV_ENGINE_ID`, and run `php index.php`. Want to see everything in action? 1. Clone this repository or copy the `index.php` from the **demo** below into an empty directory. 2. Run `composer install` to pull in the SDK. -3. Populate the `LINGODOTDEV_API_KEY` environment variable with your key. +3. Populate the `LINGODOTDEV_API_KEY` and `LINGODOTDEV_ENGINE_ID` environment variables. 4. Execute the script with `php index.php` and observe the output. `index.php` demo: @@ -211,6 +248,7 @@ use LingoDotDev\Sdk\LingoDotDevEngine; $engine = new LingoDotDevEngine([ 'apiKey' => getenv('LINGODOTDEV_API_KEY'), + 'engineId' => getenv('LINGODOTDEV_ENGINE_ID'), ]); // 1. Text @@ -218,15 +256,13 @@ $helloEs = $engine->localizeText('Hello world!', [ 'sourceLocale' => 'en', 'targetLocale' => 'es', ]); - -echo "Text ES ⇒ $helloEs\n\n"; +echo "Text ES: $helloEs\n\n"; // 2. Object -$object = [ +$objectFr = $engine->localizeObject([ 'greeting' => 'Good morning', 'farewell' => 'Good night', -]; -$objectFr = $engine->localizeObject($object, [ +], [ 'sourceLocale' => 'en', 'targetLocale' => 'fr', ]); @@ -244,7 +280,6 @@ print_r($chatJa); // 4. Detect language $lang = $engine->recognizeLocale('Ciao mondo'); - echo "Detected: $lang\n"; ``` diff --git a/composer.json b/composer.json index 2399fef..0755779 100644 --- a/composer.json +++ b/composer.json @@ -11,8 +11,7 @@ ], "require": { "php": "^8.1", - "guzzlehttp/guzzle": "^7.0", - "respect/validation": "^2.0" + "guzzlehttp/guzzle": "^7.0" }, "require-dev": { "phpunit/phpunit": "^9.0" diff --git a/src/LingoDotDevEngine.php b/src/LingoDotDevEngine.php index ea26a91..8a407cb 100644 --- a/src/LingoDotDevEngine.php +++ b/src/LingoDotDevEngine.php @@ -13,7 +13,6 @@ use GuzzleHttp\Client; use GuzzleHttp\Exception\RequestException; -use Respect\Validation\Validator as v; /** * LingoDotDevEngine class for interacting with the LingoDotDev API @@ -42,16 +41,23 @@ class LingoDotDevEngine */ private $_httpClient; + /** + * Unique session ID generated once per engine instance + * + * @var string + */ + private $_sessionId; + /** * Create a new LingoDotDevEngine instance - * + * * @param array $config Configuration options for the Engine */ public function __construct(array $config = []) { $this->config = array_merge( [ - 'apiUrl' => 'https://engine.lingo.dev', + 'apiUrl' => 'https://api.lingo.dev', 'batchSize' => 25, 'idealBatchItemSize' => 250 ], $config @@ -61,6 +67,10 @@ public function __construct(array $config = []) throw new \InvalidArgumentException('API key is required'); } + if (empty($this->config['engineId'])) { + throw new \InvalidArgumentException('Engine ID is required'); + } + if (!filter_var($this->config['apiUrl'], FILTER_VALIDATE_URL)) { throw new \InvalidArgumentException('API URL must be a valid URL'); } @@ -73,12 +83,14 @@ public function __construct(array $config = []) throw new \InvalidArgumentException('Ideal batch item size must be an integer between 1 and 2500'); } + $this->_sessionId = $this->_createId(); + $this->_httpClient = new Client( [ 'base_uri' => $this->config['apiUrl'], 'headers' => [ 'Content-Type' => 'application/json; charset=utf-8', - 'Authorization' => 'Bearer ' . $this->config['apiKey'] + 'X-API-Key' => $this->config['apiKey'] ] ] ); @@ -86,12 +98,12 @@ public function __construct(array $config = []) /** * Localize content using the Lingo.dev API - * + * * @param array $payload The content to be localized * @param array $params Localization parameters including source/target locales and fast mode option * @param callable|null $progressCallback Optional callback function to report progress (0-100) - * - * @return array Localized content + * + * @return array Localized content * @internal */ protected function localizeRaw(array $payload, array $params, ?callable $progressCallback = null): array @@ -111,8 +123,6 @@ protected function localizeRaw(array $payload, array $params, ?callable $progres $chunkedPayload = $this->_extractPayloadChunks($payload); $processedPayloadChunks = []; - $workflowId = $this->_createId(); - for ($i = 0; $i < count($chunkedPayload); $i++) { $chunk = $chunkedPayload[$i]; $percentageCompleted = round((($i + 1) / count($chunkedPayload)) * 100); @@ -121,10 +131,9 @@ protected function localizeRaw(array $payload, array $params, ?callable $progres $params['sourceLocale'] ?? null, $params['targetLocale'], [ - 'data' => $chunk, + 'data' => $chunk, 'reference' => $params['reference'] ?? null ], - $workflowId, $params['fast'] ?? false ); @@ -140,41 +149,35 @@ protected function localizeRaw(array $payload, array $params, ?callable $progres /** * Localize a single chunk of content - * + * * @param string|null $sourceLocale Source locale * @param string $targetLocale Target locale * @param array $payload Payload containing the chunk to be localized - * @param string $workflowId Workflow ID * @param bool $fast Whether to use fast mode - * + * * @return array Localized chunk */ - private function _localizeChunk(?string $sourceLocale, string $targetLocale, array $payload, string $workflowId, bool $fast): array + private function _localizeChunk(?string $sourceLocale, string $targetLocale, array $payload, bool $fast): array { try { + $url = '/process/' . $this->config['engineId'] . '/localize'; $requestBody = [ - 'params' => [ - 'workflowId' => $workflowId, - 'fast' => $fast - ], - 'locale' => [ - 'source' => $sourceLocale, - 'target' => $targetLocale - ], - 'data' => $payload['data'] + 'params' => ['fast' => $fast], + 'sourceLocale' => $sourceLocale, + 'targetLocale' => $targetLocale, + 'data' => $payload['data'], + 'sessionId' => $this->_sessionId, ]; - + if (isset($payload['reference']) && $payload['reference'] !== null) { if (!is_array($payload['reference'])) { throw new \InvalidArgumentException('Reference must be an array'); } $requestBody['reference'] = $payload['reference']; - } else { - $requestBody['reference'] = (object)[]; } - + $response = $this->_httpClient->post( - '/i18n', [ + $url, [ 'json' => $requestBody ] ); @@ -190,7 +193,7 @@ private function _localizeChunk(?string $sourceLocale, string $targetLocale, arr if ($e->hasResponse()) { $statusCode = $e->getResponse()->getStatusCode(); $responseBody = $e->getResponse()->getBody()->getContents(); - + if ($statusCode === 400) { throw new \InvalidArgumentException('Invalid request: ' . $e->getMessage()); } else { @@ -205,9 +208,9 @@ private function _localizeChunk(?string $sourceLocale, string $targetLocale, arr /** * Extract payload chunks based on the ideal chunk size - * + * * @param array $payload The payload to be chunked - * + * * @return array An array of payload chunks */ private function _extractPayloadChunks(array $payload): array @@ -218,18 +221,18 @@ private function _extractPayloadChunks(array $payload): array $payloadEntries = $payload; $keys = array_keys($payloadEntries); - + for ($i = 0; $i < count($keys); $i++) { $key = $keys[$i]; $value = $payloadEntries[$key]; - + $currentChunk[$key] = $value; $currentChunkItemCount++; $currentChunkSize = $this->_countWordsInRecord($currentChunk); - - if ($currentChunkSize > $this->config['idealBatchItemSize'] - || $currentChunkItemCount >= $this->config['batchSize'] + + if ($currentChunkSize > $this->config['idealBatchItemSize'] + || $currentChunkItemCount >= $this->config['batchSize'] || $i === count($keys) - 1 ) { $result[] = $currentChunk; @@ -243,9 +246,9 @@ private function _extractPayloadChunks(array $payload): array /** * Count words in a record or array - * + * * @param mixed $payload The payload to count words in - * + * * @return int The total number of words */ private function _countWordsInRecord($payload): int @@ -271,7 +274,7 @@ private function _countWordsInRecord($payload): int /** * Generate a unique ID - * + * * @return string Unique ID */ private function _createId(): string @@ -281,14 +284,14 @@ private function _createId(): string /** * Localize a typical PHP array or object - * + * * @param array $obj The object to be localized (strings will be extracted and translated) * @param array $params Localization parameters: * - sourceLocale: The source language code (e.g., 'en') * - targetLocale: The target language code (e.g., 'es') * - fast: Optional boolean to enable fast mode * @param callable|null $progressCallback Optional callback function to report progress (0-100) - * + * * @return array A new object with the same structure but localized string values */ public function localizeObject(array $obj, array $params, ?callable $progressCallback = null): array @@ -296,7 +299,7 @@ public function localizeObject(array $obj, array $params, ?callable $progressCal if (!isset($params['targetLocale'])) { throw new \InvalidArgumentException('Target locale is required'); } - + return $this->localizeRaw($obj, $params, function($progress, $chunk, $processedChunk) use ($progressCallback) { if ($progressCallback) { $progressCallback($progress, $chunk, $processedChunk); @@ -306,14 +309,14 @@ public function localizeObject(array $obj, array $params, ?callable $progressCal /** * Localize a single text string - * + * * @param string $text The text string to be localized * @param array $params Localization parameters: * - sourceLocale: The source language code (e.g., 'en') * - targetLocale: The target language code (e.g., 'es') * - fast: Optional boolean to enable fast mode * @param callable|null $progressCallback Optional callback function to report progress (0-100) - * + * * @return string The localized text string */ public function localizeText(string $text, array $params, ?callable $progressCallback = null): string @@ -321,25 +324,25 @@ public function localizeText(string $text, array $params, ?callable $progressCal if (!isset($params['targetLocale'])) { throw new \InvalidArgumentException('Target locale is required'); } - + $response = $this->localizeRaw(['text' => $text], $params, function($progress, $chunk, $processedChunk) use ($progressCallback) { if ($progressCallback) { $progressCallback($progress); } }); - + return $response['text'] ?? ''; } /** * Localize a text string to multiple target locales - * + * * @param string $text The text string to be localized * @param array $params Localization parameters: * - sourceLocale: The source language code (e.g., 'en') * - targetLocales: An array of target language codes (e.g., ['es', 'fr']) * - fast: Optional boolean to enable fast mode - * + * * @return array An array of localized text strings */ public function batchLocalizeText(string $text, array $params): array @@ -368,14 +371,14 @@ public function batchLocalizeText(string $text, array $params): array /** * Localize a chat sequence while preserving speaker names - * + * * @param array $chat Array of chat messages, each with 'name' and 'text' properties * @param array $params Localization parameters: * - sourceLocale: The source language code (e.g., 'en') * - targetLocale: The target language code (e.g., 'es') * - fast: Optional boolean to enable fast mode * @param callable|null $progressCallback Optional callback function to report progress (0-100) - * + * * @return array Array of localized chat messages with preserved structure */ public function localizeChat(array $chat, array $params, ?callable $progressCallback = null): array @@ -409,9 +412,9 @@ public function localizeChat(array $chat, array $params, ?callable $progressCall /** * Detect the language of a given text - * + * * @param string $text The text to analyze - * + * * @return string Locale code (e.g., 'en', 'es', 'fr') */ public function recognizeLocale(string $text): string @@ -419,24 +422,23 @@ public function recognizeLocale(string $text): string if (empty(trim($text))) { throw new \InvalidArgumentException('Text cannot be empty'); } - + try { $response = $this->_httpClient->post( - '/recognize', [ + '/process/recognize', [ 'json' => ['text' => $text] ] ); $jsonResponse = json_decode($response->getBody()->getContents(), true); - + if (!isset($jsonResponse['locale'])) { throw new \RuntimeException('Invalid response from API: locale not found'); } - + return $jsonResponse['locale']; } catch (RequestException $e) { if ($e->hasResponse()) { - $statusCode = $e->getResponse()->getStatusCode(); $responseBody = $e->getResponse()->getBody()->getContents(); $errorData = json_decode($responseBody, true); $errorMessage = isset($errorData['message']) ? $errorData['message'] : $e->getMessage(); diff --git a/test-all-methods.php b/test-all-methods.php index abe2c5f..576ee24 100644 --- a/test-all-methods.php +++ b/test-all-methods.php @@ -1,11 +1,11 @@ + * to ensure they work correctly. + * + * Usage: php test-all-methods.php */ require "vendor/autoload.php"; @@ -14,36 +14,45 @@ use GuzzleHttp\Exception\RequestException; $apiKey = $argv[1] ?? null; +$engineId = $argv[2] ?? null; -if (!$apiKey) { - echo "Error: API key is required. Pass it as a command-line argument.\n"; +if (!$apiKey || !$engineId) { + echo "Usage: php test-all-methods.php \n"; exit(1); } $engine = new LingoDotDevEngine([ "apiKey" => $apiKey, + "engineId" => $engineId, ]); +$passed = 0; +$failed = 0; + function runTest($name, $callback) { + global $passed, $failed; echo "\n=== Testing $name ===\n"; try { $result = $callback(); echo "✅ Test passed!\n"; - echo "Result: " . json_encode($result, JSON_PRETTY_PRINT) . "\n"; + echo "Result: " . json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n"; + $passed++; return true; } catch (\Exception $e) { echo "❌ Test failed!\n"; echo "Error: " . $e->getMessage() . "\n"; - + if ($e instanceof RequestException && $e->hasResponse()) { $response = $e->getResponse(); echo "Status Code: " . $response->getStatusCode() . "\n"; echo "Response Body: " . $response->getBody() . "\n"; } + $failed++; return false; } } +// 1. localizeText runTest("localizeText", function() use ($engine) { return $engine->localizeText("Hello, this is my first localization with Lingo.dev!", [ "sourceLocale" => "en", @@ -51,6 +60,16 @@ function runTest($name, $callback) { ]); }); +// 2. localizeText with fast mode +runTest("localizeText (fast mode)", function() use ($engine) { + return $engine->localizeText("The quick brown fox jumps over the lazy dog.", [ + "sourceLocale" => "en", + "targetLocale" => "fr", + "fast" => true, + ]); +}); + +// 3. localizeObject runTest("localizeObject", function() use ($engine) { return $engine->localizeObject([ "greeting" => "Hello", @@ -65,6 +84,7 @@ function runTest($name, $callback) { ]); }); +// 4. localizeChat runTest("localizeChat", function() use ($engine) { return $engine->localizeChat([ ["name" => "Alice", "text" => "Hello, how are you?"], @@ -76,6 +96,7 @@ function runTest($name, $callback) { ]); }); +// 5. batchLocalizeText runTest("batchLocalizeText", function() use ($engine) { return $engine->batchLocalizeText("Hello, world!", [ "sourceLocale" => "en", @@ -83,14 +104,33 @@ function runTest($name, $callback) { ]); }); +// 6. recognizeLocale runTest("recognizeLocale", function() use ($engine) { return $engine->recognizeLocale("Bonjour le monde"); }); +// 7. localizeObject with reference +runTest("localizeObject with reference", function() use ($engine) { + return $engine->localizeObject([ + "greeting" => "Hello", + "farewell" => "Goodbye", + ], [ + "sourceLocale" => "en", + "targetLocale" => "es", + "reference" => [ + "fr" => [ + "greeting" => "Bonjour", + "farewell" => "Au revoir" + ] + ], + ]); +}); + +// 8. Progress callback runTest("Progress Callback", function() use ($engine) { $progressCalled = false; $progressValue = 0; - + $result = $engine->localizeText("Hello, this is a test with progress callback!", [ "sourceLocale" => "en", "targetLocale" => "es", @@ -99,11 +139,11 @@ function runTest($name, $callback) { $progressValue = $progress; echo "Progress: $progress%\n"; }); - + if (!$progressCalled) { throw new \Exception("Progress callback was not called"); } - + return [ "result" => $result, "progressCalled" => $progressCalled, @@ -111,4 +151,5 @@ function runTest($name, $callback) { ]; }); -echo "\n=== All tests completed ===\n"; +echo "\n=== All tests completed: $passed passed, $failed failed ===\n"; +exit($failed > 0 ? 1 : 0); diff --git a/tests/LingoDotDevEngineTest.php b/tests/LingoDotDevEngineTest.php index 59bd1ee..c7d6c4c 100644 --- a/tests/LingoDotDevEngineTest.php +++ b/tests/LingoDotDevEngineTest.php @@ -41,15 +41,61 @@ private function _createMockEngine($responses) { $mock = new MockHandler($responses); $handlerStack = HandlerStack::create($mock); - $client = new Client(['handler' => $handlerStack]); + $client = new Client([ + 'handler' => $handlerStack, + 'headers' => [ + 'Content-Type' => 'application/json; charset=utf-8', + 'X-API-Key' => 'test-api-key' + ] + ]); + + $engine = new LingoDotDevEngine([ + 'apiKey' => 'test-api-key', + 'engineId' => 'test-engine' + ]); + + $reflection = new ReflectionClass($engine); + $property = $reflection->getProperty('_httpClient'); + $property->setAccessible(true); + $property->setValue($engine, $client); + + return $engine; + } + + /** + * Creates a mock engine with history tracking + * + * @param array $responses Array of mock responses + * @param array &$history Reference to history array for capturing requests + * @param array $config Engine config overrides + * + * @return LingoDotDevEngine Mocked engine instance + */ + private function _createMockEngineWithHistory($responses, &$history, $config = []) + { + $mock = new MockHandler($responses); + $handlerStack = HandlerStack::create($mock); + $handlerStack->push(\GuzzleHttp\Middleware::history($history)); + $engineConfig = array_merge([ + 'apiKey' => 'test-api-key', + 'engineId' => 'test-engine' + ], $config); + + $client = new Client([ + 'handler' => $handlerStack, + 'headers' => [ + 'Content-Type' => 'application/json; charset=utf-8', + 'X-API-Key' => $engineConfig['apiKey'] + ] + ]); + + $engine = new LingoDotDevEngine($engineConfig); - $engine = new LingoDotDevEngine(['apiKey' => 'test-api-key']); - $reflection = new ReflectionClass($engine); $property = $reflection->getProperty('_httpClient'); $property->setAccessible(true); $property->setValue($engine, $client); - + return $engine; } @@ -60,16 +106,31 @@ private function _createMockEngine($responses) */ public function testConstructorWithValidConfig() { - $engine = new LingoDotDevEngine(['apiKey' => 'test-api-key']); + $engine = new LingoDotDevEngine([ + 'apiKey' => 'test-api-key', + 'engineId' => 'test-engine' + ]); $this->assertInstanceOf(LingoDotDevEngine::class, $engine); } /** - * Tests constructor with invalid configuration + * Tests constructor requires engineId * * @return void */ - public function testConstructorWithInvalidConfig() + public function testConstructorRequiresEngineId() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Engine ID is required'); + new LingoDotDevEngine(['apiKey' => 'test-api-key']); + } + + /** + * Tests constructor requires apiKey + * + * @return void + */ + public function testConstructorRequiresApiKey() { $this->expectException(\InvalidArgumentException::class); new LingoDotDevEngine([]); @@ -274,7 +335,7 @@ public function testErrorHandling() ] ); } - + /** * Tests the reference parameter handling * @@ -282,43 +343,30 @@ public function testErrorHandling() */ public function testReferenceParameterHandling() { - $mock = new MockHandler([ - new Response(200, [], json_encode(['data' => ['text' => 'Hola, mundo!']])) - ]); - - $requestData = null; $history = []; - $historyMiddleware = \GuzzleHttp\Middleware::history($history); - - $handlerStack = HandlerStack::create($mock); - $handlerStack->push($historyMiddleware); - - $client = new Client(['handler' => $handlerStack]); - - $engine = new LingoDotDevEngine(['apiKey' => 'test-api-key']); - - $reflection = new ReflectionClass($engine); - $property = $reflection->getProperty('_httpClient'); - $property->setAccessible(true); - $property->setValue($engine, $client); - + $engine = $this->_createMockEngineWithHistory( + [new Response(200, [], json_encode(['data' => ['text' => 'Hola, mundo!']]))], + $history + ); + $engine->localizeText('Hello, world!', [ 'sourceLocale' => 'en', 'targetLocale' => 'es' ]); - + $this->assertCount(1, $history); $request = $history[0]['request']; $requestBody = json_decode($request->getBody()->getContents(), true); - - $this->assertArrayHasKey('reference', $requestBody); - $this->assertEquals([], $requestBody['reference']); - + + // vNext omits reference when not provided + $this->assertArrayNotHasKey('reference', $requestBody); + $this->assertArrayHasKey('params', $requestBody); - $this->assertArrayHasKey('locale', $requestBody); + $this->assertArrayHasKey('sourceLocale', $requestBody); + $this->assertArrayHasKey('targetLocale', $requestBody); $this->assertArrayHasKey('data', $requestBody); - $this->assertEquals('en', $requestBody['locale']['source']); - $this->assertEquals('es', $requestBody['locale']['target']); + $this->assertEquals('en', $requestBody['sourceLocale']); + $this->assertEquals('es', $requestBody['targetLocale']); $this->assertEquals(['text' => 'Hello, world!'], $requestBody['data']); } @@ -357,4 +405,214 @@ public function testProgressCallback() $this->assertTrue($progressCalled); $this->assertEquals(100, $progressValue); } + + /** + * Tests default apiUrl is api.lingo.dev + * + * @return void + */ + public function testConfigDefaultApiUrl() + { + $engine = new LingoDotDevEngine([ + 'apiKey' => 'test-api-key', + 'engineId' => 'my-engine-id' + ]); + + $reflection = new ReflectionClass($engine); + $property = $reflection->getProperty('config'); + $property->setAccessible(true); + $config = $property->getValue($engine); + + $this->assertEquals('https://api.lingo.dev', $config['apiUrl']); + $this->assertEquals('my-engine-id', $config['engineId']); + } + + /** + * Tests that explicit apiUrl is preserved even with engineId + * + * @return void + */ + public function testConfigExplicitApiUrl() + { + $engine = new LingoDotDevEngine([ + 'apiKey' => 'test-api-key', + 'engineId' => 'my-engine-id', + 'apiUrl' => 'https://custom.api.com' + ]); + + $reflection = new ReflectionClass($engine); + $property = $reflection->getProperty('config'); + $property->setAccessible(true); + $config = $property->getValue($engine); + + $this->assertEquals('https://custom.api.com', $config['apiUrl']); + } + + /** + * Tests that X-API-Key header is used + * + * @return void + */ + public function testXApiKeyHeader() + { + $history = []; + $engine = $this->_createMockEngineWithHistory( + [new Response(200, [], json_encode(['data' => ['text' => 'Hola']]))], + $history, + ['engineId' => 'my-engine'] + ); + + $engine->localizeText('Hello', [ + 'sourceLocale' => 'en', + 'targetLocale' => 'es' + ]); + + $this->assertCount(1, $history); + $request = $history[0]['request']; + $this->assertEquals('test-api-key', $request->getHeaderLine('X-API-Key')); + $this->assertEmpty($request->getHeaderLine('Authorization')); + } + + /** + * Tests localize chunk URL and body format + * + * @return void + */ + public function testLocalizeChunkUrlAndBody() + { + $history = []; + $engine = $this->_createMockEngineWithHistory( + [new Response(200, [], json_encode(['data' => ['text' => 'Hola']]))], + $history, + ['engineId' => 'my-engine'] + ); + + $engine->localizeText('Hello', [ + 'sourceLocale' => 'en', + 'targetLocale' => 'es', + 'fast' => true + ]); + + $this->assertCount(1, $history); + $request = $history[0]['request']; + + // Check URL + $this->assertStringContainsString('/process/my-engine/localize', $request->getUri()->getPath()); + + // Check body format + $body = json_decode($request->getBody()->getContents(), true); + $this->assertEquals(['fast' => true], $body['params']); + $this->assertEquals('en', $body['sourceLocale']); + $this->assertEquals('es', $body['targetLocale']); + $this->assertEquals(['text' => 'Hello'], $body['data']); + $this->assertArrayHasKey('sessionId', $body); + $this->assertNotEmpty($body['sessionId']); + + // vNext should NOT have workflowId or locale object + $this->assertArrayNotHasKey('workflowId', $body['params']); + $this->assertArrayNotHasKey('locale', $body); + } + + /** + * Tests that sessionId is consistent across multiple requests + * + * @return void + */ + public function testSessionIdConsistentAcrossRequests() + { + $history = []; + $engine = $this->_createMockEngineWithHistory( + [ + new Response(200, [], json_encode(['data' => ['text' => 'Hola']])), + new Response(200, [], json_encode(['data' => ['text' => 'Mundo']])) + ], + $history, + ['engineId' => 'my-engine'] + ); + + $engine->localizeText('Hello', [ + 'sourceLocale' => 'en', + 'targetLocale' => 'es' + ]); + $engine->localizeText('World', [ + 'sourceLocale' => 'en', + 'targetLocale' => 'es' + ]); + + $body1 = json_decode($history[0]['request']->getBody()->getContents(), true); + $body2 = json_decode($history[1]['request']->getBody()->getContents(), true); + $this->assertEquals($body1['sessionId'], $body2['sessionId']); + } + + /** + * Tests that reference is omitted when not provided + * + * @return void + */ + public function testOmitsReferenceWhenNotProvided() + { + $history = []; + $engine = $this->_createMockEngineWithHistory( + [new Response(200, [], json_encode(['data' => ['text' => 'Hola']]))], + $history, + ['engineId' => 'my-engine'] + ); + + $engine->localizeText('Hello', [ + 'sourceLocale' => 'en', + 'targetLocale' => 'es' + ]); + + $body = json_decode($history[0]['request']->getBody()->getContents(), true); + $this->assertArrayNotHasKey('reference', $body); + } + + /** + * Tests that reference is included when provided + * + * @return void + */ + public function testIncludesReferenceWhenProvided() + { + $history = []; + $engine = $this->_createMockEngineWithHistory( + [new Response(200, [], json_encode(['data' => ['greeting' => 'Hola']]))], + $history, + ['engineId' => 'my-engine'] + ); + + $engine->localizeObject( + ['greeting' => 'Hello'], + [ + 'sourceLocale' => 'en', + 'targetLocale' => 'es', + 'reference' => ['fr' => ['greeting' => 'Bonjour']] + ] + ); + + $body = json_decode($history[0]['request']->getBody()->getContents(), true); + $this->assertArrayHasKey('reference', $body); + $this->assertEquals(['fr' => ['greeting' => 'Bonjour']], $body['reference']); + } + + /** + * Tests recognizeLocale URL + * + * @return void + */ + public function testRecognizeLocaleUrl() + { + $history = []; + $engine = $this->_createMockEngineWithHistory( + [new Response(200, [], json_encode(['locale' => 'fr']))], + $history, + ['engineId' => 'my-engine'] + ); + + $result = $engine->recognizeLocale('Bonjour le monde'); + + $this->assertEquals('fr', $result); + $this->assertStringContainsString('/process/recognize', $history[0]['request']->getUri()->getPath()); + } + } From 24db355ec23eef4eb085fc475f124b3ef88c5092 Mon Sep 17 00:00:00 2001 From: AndreyHirsa Date: Thu, 5 Mar 2026 23:36:20 +0300 Subject: [PATCH 2/2] feat: make engineId optional, use single /process/localize endpoint --- README.md | 34 +++++++----------- src/LingoDotDevEngine.php | 10 +++--- test-all-methods.php | 40 ++++++++++----------- tests/LingoDotDevEngineTest.php | 64 +++++++++++++++++++++++++++++---- 4 files changed, 95 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index e273028..236a9ce 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ composer require lingodotdev/sdk ## Basic Usage -After installing the package, bootstrap the engine with your API key and Engine ID: +After installing the package, bootstrap the engine with your API key: ```php require 'vendor/autoload.php'; @@ -21,7 +21,7 @@ use LingoDotDev\Sdk\LingoDotDevEngine; $engine = new LingoDotDevEngine([ 'apiKey' => 'your-api-key', // replace with your actual key - 'engineId' => 'your-engine-id', // replace with your actual engine id + 'engineId' => 'your-engine-id', // optional — override the default engine ]); ``` @@ -30,7 +30,7 @@ $engine = new LingoDotDevEngine([ | Option | Type | Required | Default | Description | |---|---|---|---|---| | `apiKey` | string | Yes | — | Your Lingo.dev API key | -| `engineId` | string | Yes | — | Your Lingo.dev Engine ID | +| `engineId` | string | No | — | Your Lingo.dev Engine ID | | `apiUrl` | string | No | `https://api.lingo.dev` | API base URL | | `batchSize` | int | No | `25` | Max items per chunk (1–250) | | `idealBatchItemSize` | int | No | `250` | Max words per chunk (1–2500) | @@ -84,10 +84,10 @@ Follow these steps to create a new PHP project that uses the Lingo.dev SDK: use LingoDotDev\Sdk\LingoDotDevEngine; -// Initialize the SDK with your API key and Engine ID +// Initialize the SDK with your API key $engine = new LingoDotDevEngine([ 'apiKey' => 'your-api-key', - 'engineId' => 'your-engine-id', + 'engineId' => 'your-engine-id', // optional ]); ``` @@ -104,17 +104,6 @@ $localizedText = $engine->localizeText('Hello, world!', [ // Output: "¡Hola, mundo!" ``` -You can enable fast mode for quicker (but potentially lower quality) translations: - -```php -// Localize with fast mode enabled -$localizedText = $engine->localizeText('Hello, world!', [ - 'sourceLocale' => 'en', - 'targetLocale' => 'es', - 'fast' => true, -]); -``` - ### Object Localization Translate an array of strings while preserving the structure: @@ -228,13 +217,13 @@ $engine->localizeText('Hello, world!', [ ## Demo App -If you prefer to start with a minimal example instead of the detailed scenarios above, create **index.php** in an empty folder, copy the following snippet, install dependencies with `composer require lingodotdev/sdk`, set `LINGODOTDEV_API_KEY` and `LINGODOTDEV_ENGINE_ID`, and run `php index.php`. +If you prefer to start with a minimal example instead of the detailed scenarios above, create **index.php** in an empty folder, copy the following snippet, install dependencies with `composer require lingodotdev/sdk`, set `LINGODOTDEV_API_KEY` (and optionally `LINGODOTDEV_ENGINE_ID`), and run `php index.php`. Want to see everything in action? 1. Clone this repository or copy the `index.php` from the **demo** below into an empty directory. 2. Run `composer install` to pull in the SDK. -3. Populate the `LINGODOTDEV_API_KEY` and `LINGODOTDEV_ENGINE_ID` environment variables. +3. Populate the `LINGODOTDEV_API_KEY` environment variable (and optionally `LINGODOTDEV_ENGINE_ID`). 4. Execute the script with `php index.php` and observe the output. `index.php` demo: @@ -246,10 +235,13 @@ require 'vendor/autoload.php'; use LingoDotDev\Sdk\LingoDotDevEngine; -$engine = new LingoDotDevEngine([ +$config = [ 'apiKey' => getenv('LINGODOTDEV_API_KEY'), - 'engineId' => getenv('LINGODOTDEV_ENGINE_ID'), -]); +]; +if (getenv('LINGODOTDEV_ENGINE_ID')) { + $config['engineId'] = getenv('LINGODOTDEV_ENGINE_ID'); +} +$engine = new LingoDotDevEngine($config); // 1. Text $helloEs = $engine->localizeText('Hello world!', [ diff --git a/src/LingoDotDevEngine.php b/src/LingoDotDevEngine.php index 8a407cb..3c617dc 100644 --- a/src/LingoDotDevEngine.php +++ b/src/LingoDotDevEngine.php @@ -67,10 +67,6 @@ public function __construct(array $config = []) throw new \InvalidArgumentException('API key is required'); } - if (empty($this->config['engineId'])) { - throw new \InvalidArgumentException('Engine ID is required'); - } - if (!filter_var($this->config['apiUrl'], FILTER_VALIDATE_URL)) { throw new \InvalidArgumentException('API URL must be a valid URL'); } @@ -160,7 +156,7 @@ protected function localizeRaw(array $payload, array $params, ?callable $progres private function _localizeChunk(?string $sourceLocale, string $targetLocale, array $payload, bool $fast): array { try { - $url = '/process/' . $this->config['engineId'] . '/localize'; + $url = '/process/localize'; $requestBody = [ 'params' => ['fast' => $fast], 'sourceLocale' => $sourceLocale, @@ -169,6 +165,10 @@ private function _localizeChunk(?string $sourceLocale, string $targetLocale, arr 'sessionId' => $this->_sessionId, ]; + if (!empty($this->config['engineId'])) { + $requestBody['engineId'] = $this->config['engineId']; + } + if (isset($payload['reference']) && $payload['reference'] !== null) { if (!is_array($payload['reference'])) { throw new \InvalidArgumentException('Reference must be an array'); diff --git a/test-all-methods.php b/test-all-methods.php index 576ee24..f1da432 100644 --- a/test-all-methods.php +++ b/test-all-methods.php @@ -5,7 +5,7 @@ * This script tests all available methods in the PHP SDK with real API calls * to ensure they work correctly. * - * Usage: php test-all-methods.php + * Usage: php test-all-methods.php [engine_id] [api_url] */ require "vendor/autoload.php"; @@ -15,16 +15,23 @@ $apiKey = $argv[1] ?? null; $engineId = $argv[2] ?? null; +$apiUrl = $argv[3] ?? null; -if (!$apiKey || !$engineId) { - echo "Usage: php test-all-methods.php \n"; +if (!$apiKey) { + echo "Usage: php test-all-methods.php [engine_id] [api_url]\n"; exit(1); } -$engine = new LingoDotDevEngine([ +$config = [ "apiKey" => $apiKey, - "engineId" => $engineId, -]); +]; +if ($engineId) { + $config["engineId"] = $engineId; +} +if ($apiUrl) { + $config["apiUrl"] = $apiUrl; +} +$engine = new LingoDotDevEngine($config); $passed = 0; $failed = 0; @@ -60,16 +67,7 @@ function runTest($name, $callback) { ]); }); -// 2. localizeText with fast mode -runTest("localizeText (fast mode)", function() use ($engine) { - return $engine->localizeText("The quick brown fox jumps over the lazy dog.", [ - "sourceLocale" => "en", - "targetLocale" => "fr", - "fast" => true, - ]); -}); - -// 3. localizeObject +// 2. localizeObject runTest("localizeObject", function() use ($engine) { return $engine->localizeObject([ "greeting" => "Hello", @@ -84,7 +82,7 @@ function runTest($name, $callback) { ]); }); -// 4. localizeChat +// 3. localizeChat runTest("localizeChat", function() use ($engine) { return $engine->localizeChat([ ["name" => "Alice", "text" => "Hello, how are you?"], @@ -96,7 +94,7 @@ function runTest($name, $callback) { ]); }); -// 5. batchLocalizeText +// 4. batchLocalizeText runTest("batchLocalizeText", function() use ($engine) { return $engine->batchLocalizeText("Hello, world!", [ "sourceLocale" => "en", @@ -104,12 +102,12 @@ function runTest($name, $callback) { ]); }); -// 6. recognizeLocale +// 5. recognizeLocale runTest("recognizeLocale", function() use ($engine) { return $engine->recognizeLocale("Bonjour le monde"); }); -// 7. localizeObject with reference +// 6. localizeObject with reference runTest("localizeObject with reference", function() use ($engine) { return $engine->localizeObject([ "greeting" => "Hello", @@ -126,7 +124,7 @@ function runTest($name, $callback) { ]); }); -// 8. Progress callback +// 7. Progress callback runTest("Progress Callback", function() use ($engine) { $progressCalled = false; $progressValue = 0; diff --git a/tests/LingoDotDevEngineTest.php b/tests/LingoDotDevEngineTest.php index c7d6c4c..375c20b 100644 --- a/tests/LingoDotDevEngineTest.php +++ b/tests/LingoDotDevEngineTest.php @@ -114,15 +114,16 @@ public function testConstructorWithValidConfig() } /** - * Tests constructor requires engineId + * Tests constructor works without engineId * * @return void */ - public function testConstructorRequiresEngineId() + public function testConstructorWithoutEngineId() { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Engine ID is required'); - new LingoDotDevEngine(['apiKey' => 'test-api-key']); + $engine = new LingoDotDevEngine([ + 'apiKey' => 'test-api-key', + ]); + $this->assertInstanceOf(LingoDotDevEngine::class, $engine); } /** @@ -497,7 +498,8 @@ public function testLocalizeChunkUrlAndBody() $request = $history[0]['request']; // Check URL - $this->assertStringContainsString('/process/my-engine/localize', $request->getUri()->getPath()); + $this->assertStringContainsString('/process/localize', $request->getUri()->getPath()); + $this->assertStringNotContainsString('/process/my-engine/localize', $request->getUri()->getPath()); // Check body format $body = json_decode($request->getBody()->getContents(), true); @@ -507,12 +509,62 @@ public function testLocalizeChunkUrlAndBody() $this->assertEquals(['text' => 'Hello'], $body['data']); $this->assertArrayHasKey('sessionId', $body); $this->assertNotEmpty($body['sessionId']); + $this->assertEquals('my-engine', $body['engineId']); // vNext should NOT have workflowId or locale object $this->assertArrayNotHasKey('workflowId', $body['params']); $this->assertArrayNotHasKey('locale', $body); } + /** + * Tests localize chunk URL and body format without engineId + * + * @return void + */ + public function testLocalizeChunkUrlAndBodyWithoutEngineId() + { + $history = []; + $mock = new MockHandler([ + new Response(200, [], json_encode(['data' => ['text' => 'Hola']])) + ]); + $handlerStack = HandlerStack::create($mock); + $handlerStack->push(\GuzzleHttp\Middleware::history($history)); + + $engine = new LingoDotDevEngine([ + 'apiKey' => 'test-api-key', + ]); + + $client = new Client([ + 'handler' => $handlerStack, + 'headers' => [ + 'Content-Type' => 'application/json; charset=utf-8', + 'X-API-Key' => 'test-api-key' + ] + ]); + + $reflection = new ReflectionClass($engine); + $property = $reflection->getProperty('_httpClient'); + $property->setAccessible(true); + $property->setValue($engine, $client); + + $engine->localizeText('Hello', [ + 'sourceLocale' => 'en', + 'targetLocale' => 'es', + ]); + + $this->assertCount(1, $history); + $request = $history[0]['request']; + + // Check URL is /process/localize + $this->assertStringContainsString('/process/localize', $request->getUri()->getPath()); + + // Check body omits engineId + $body = json_decode($request->getBody()->getContents(), true); + $this->assertArrayNotHasKey('engineId', $body); + $this->assertEquals('en', $body['sourceLocale']); + $this->assertEquals('es', $body['targetLocale']); + } + /** * Tests that sessionId is consistent across multiple requests *