diff --git a/README.md b/README.md index 3f99b34..236a9ce 100644 --- a/README.md +++ b/README.md @@ -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', // optional — override the default engine ]); ``` +### Configuration Options + +| Option | Type | Required | Default | Description | +|---|---|---|---|---| +| `apiKey` | string | Yes | — | Your Lingo.dev API key | +| `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) | + ### 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 @@ -77,6 +87,7 @@ use LingoDotDev\Sdk\LingoDotDevEngine; // Initialize the SDK with your API key $engine = new LingoDotDevEngine([ 'apiKey' => 'your-api-key', + 'engineId' => 'your-engine-id', // optional ]); ``` @@ -122,6 +133,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 +210,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 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` environment variable with your key. +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: @@ -209,24 +235,26 @@ require 'vendor/autoload.php'; use LingoDotDev\Sdk\LingoDotDevEngine; -$engine = new LingoDotDevEngine([ +$config = [ 'apiKey' => getenv('LINGODOTDEV_API_KEY'), -]); +]; +if (getenv('LINGODOTDEV_ENGINE_ID')) { + $config['engineId'] = getenv('LINGODOTDEV_ENGINE_ID'); +} +$engine = new LingoDotDevEngine($config); // 1. Text $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 +272,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..3c617dc 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 @@ -73,12 +79,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 +94,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 +119,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 +127,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 +145,39 @@ 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/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 (!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'); } $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..f1da432 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 [engine_id] [api_url] */ require "vendor/autoload.php"; @@ -14,36 +14,52 @@ use GuzzleHttp\Exception\RequestException; $apiKey = $argv[1] ?? null; +$engineId = $argv[2] ?? null; +$apiUrl = $argv[3] ?? null; if (!$apiKey) { - echo "Error: API key is required. Pass it as a command-line argument.\n"; + echo "Usage: php test-all-methods.php [engine_id] [api_url]\n"; exit(1); } -$engine = new LingoDotDevEngine([ +$config = [ "apiKey" => $apiKey, -]); +]; +if ($engineId) { + $config["engineId"] = $engineId; +} +if ($apiUrl) { + $config["apiUrl"] = $apiUrl; +} +$engine = new LingoDotDevEngine($config); + +$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 +67,7 @@ function runTest($name, $callback) { ]); }); +// 2. localizeObject runTest("localizeObject", function() use ($engine) { return $engine->localizeObject([ "greeting" => "Hello", @@ -65,6 +82,7 @@ function runTest($name, $callback) { ]); }); +// 3. localizeChat runTest("localizeChat", function() use ($engine) { return $engine->localizeChat([ ["name" => "Alice", "text" => "Hello, how are you?"], @@ -76,6 +94,7 @@ function runTest($name, $callback) { ]); }); +// 4. batchLocalizeText runTest("batchLocalizeText", function() use ($engine) { return $engine->batchLocalizeText("Hello, world!", [ "sourceLocale" => "en", @@ -83,14 +102,33 @@ function runTest($name, $callback) { ]); }); +// 5. recognizeLocale runTest("recognizeLocale", function() use ($engine) { return $engine->recognizeLocale("Bonjour le monde"); }); +// 6. 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" + ] + ], + ]); +}); + +// 7. 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 +137,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 +149,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..375c20b 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,32 @@ 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 works without engineId * * @return void */ - public function testConstructorWithInvalidConfig() + public function testConstructorWithoutEngineId() + { + $engine = new LingoDotDevEngine([ + 'apiKey' => 'test-api-key', + ]); + $this->assertInstanceOf(LingoDotDevEngine::class, $engine); + } + + /** + * Tests constructor requires apiKey + * + * @return void + */ + public function testConstructorRequiresApiKey() { $this->expectException(\InvalidArgumentException::class); new LingoDotDevEngine([]); @@ -274,7 +336,7 @@ public function testErrorHandling() ] ); } - + /** * Tests the reference parameter handling * @@ -282,43 +344,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 +406,265 @@ 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/localize', $request->getUri()->getPath()); + $this->assertStringNotContainsString('/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']); + $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 + * + * @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()); + } + }