diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8f4ce36 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,144 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Repository Overview + +This is the PHP SDK for the Smartling Translation API. It provides a PHP client library for interacting with Smartling's translation management platform, allowing developers to upload files for translation, download translated content, manage translation jobs, and access other Smartling API features. + +## Development Commands + +### Install Dependencies +```bash +composer install +``` + +### Run All Tests +Tests require Smartling API credentials as environment variables: +```bash +account_uid= project_id= user_id= user_key= ./vendor/bin/phpunit +``` + +### Run Specific Test Suites +```bash +# Unit tests only +./vendor/bin/phpunit --testsuite unit + +# Functional tests only (requires API credentials) +account_uid= project_id= user_id= user_key= ./vendor/bin/phpunit --testsuite functional +``` + +### Run Individual Test Files +```bash +./vendor/bin/phpunit tests/unit/SomeTest.php +``` + +## Architecture + +### Core Components + +**BaseApiAbstract** (`src/BaseApiAbstract.php`) +- Abstract base class for all API clients +- Handles HTTP communication via Guzzle +- Manages authentication via `AuthApiInterface` +- Implements automatic token refresh on 401 errors +- Provides logging support via PSR-3 `LoggerInterface` +- Standardizes error handling and response processing +- All API classes extend this base + +**Authentication Flow** +- `AuthTokenProvider` (`src/AuthApi/`) implements `AuthApiInterface` +- Manages OAuth access tokens and refresh tokens with automatic expiration handling +- Token refresh happens automatically when expired or on 401 responses +- All API clients receive an auth provider instance and use it to authenticate requests + +**API Client Pattern** +Each Smartling API endpoint has a dedicated API class: +- `FileApi` - File upload, download, and management +- `JobsApi` - Translation job management +- `BatchApi` / `BatchApiV2` - Batch operations +- `ContextApi` - Visual context and screenshots +- `ProjectApi` - Project information +- `AuditLogApi` - Audit logs +- `ProgressTrackerApi` - Translation progress tracking +- `TranslationRequestsApi` - Translation request management +- `DistributedLockServiceApi` - Distributed locking + +**Parameter Objects** +- Located in `*/Params/` directories under each API module +- Implement `ParameterInterface` or extend `BaseParameters` +- Provide type-safe parameter building for API methods +- Example: `UploadFileParameters`, `CreateJobParameters`, `DownloadFileParameters` + +### Directory Structure + +``` +src/ +├── BaseApiAbstract.php # Base class for all API clients +├── AuthApi/ # Authentication provider +├── File/ # File API (upload/download) +├── Jobs/ # Jobs API +├── Batch/ # Batch operations (v1 and v2) +├── Context/ # Visual context API +├── Project/ # Project information API +├── AuditLog/ # Audit logging API +├── ProgressTracker/ # Progress tracking API +├── TranslationRequests/ # Translation requests API +├── DistributedLockService/ # Distributed lock service +├── Parameters/ # Base parameter classes +└── Exceptions/ # Custom exceptions + +tests/ +├── unit/ # Unit tests (no API calls) +└── functional/ # Functional tests (require API credentials) + +examples/ # Usage examples for each API +``` + +### API Client Instantiation Pattern + +All API clients follow this factory pattern: +```php +$authProvider = AuthTokenProvider::create($userIdentifier, $userSecretKey); +$api = SomeApi::create($authProvider, $projectId, $logger); +``` + +Each API class: +1. Defines its own `ENDPOINT_URL` constant +2. Provides a static `create()` factory method +3. Initializes an HTTP client via `BaseApiAbstract::initializeHttpClient()` +4. Sets the auth provider via `setAuth()` + +### Error Handling + +- `SmartlingApiException` is thrown for all API errors +- Errors contain structured error information from the API response +- 401 errors trigger automatic token refresh and retry +- Sensitive data (tokens, credentials) is redacted from logs + +### Logging + +- All API clients accept an optional PSR-3 `LoggerInterface` +- Requests and responses are logged for debugging +- Sensitive data is automatically redacted in logs + +## PHP Version Support + +- PHP 7.3+ and PHP 8.x +- Uses PSR-4 autoloading +- Requires Guzzle 6.x or 7.x for HTTP client +- Requires PSR Log 1.x or 3.x for logging + +## Testing Strategy + +- **Unit tests** (`tests/unit/`) - Test logic without making actual API calls +- **Functional tests** (`tests/functional/`) - Integration tests that call the real Smartling API +- Functional tests require valid Smartling credentials passed as environment variables +- Test resources are stored in `tests/resources/` + +## Examples + +The `examples/` directory contains working examples for each API: +- Run with: `php file-example.php --project-id=XXX --user-id=XXX --secret-key=XXX` +- Each example demonstrates common operations for that API +- Examples are self-contained and include inline documentation diff --git a/examples/file-translations-example.php b/examples/file-translations-example.php new file mode 100644 index 0000000..1918fd1 --- /dev/null +++ b/examples/file-translations-example.php @@ -0,0 +1,187 @@ +uploadFile($filePath, $fileName, $fileType); + $fileUid = $uploadResult['fileUid']; + echo " ✓ File uploaded successfully\n"; + echo " File UID: {$fileUid}\n\n"; + + // Step 3: Initiate translation + echo "3. Initiating translation to Spanish, French, and German...\n"; + $translateParams = new TranslateFileParameters(); + $translateParams + ->setSourceLocaleId('en') + ->setTargetLocaleIds(['es', 'fr', 'de']); + + $translateResult = $api->translateFile($fileUid, $translateParams); + $mtUid = $translateResult['mtUid']; + echo " ✓ Translation initiated\n"; + echo " MT UID: {$mtUid}\n\n"; + + // Step 4: Poll translation progress + echo "4. Polling translation progress...\n"; + $maxAttempts = 60; + $pollInterval = 5; // seconds + $completed = false; + + for ($i = 0; $i < $maxAttempts; $i++) { + $progress = $api->getTranslationProgress($fileUid, $mtUid); + $status = $progress['state']; + + echo " Status: {$status}"; + + if (isset($progress['completedLocales'])) { + $completedCount = count($progress['completedLocales']); + $totalCount = count($translateParams->exportToArray()['targetLocaleIds']); + echo " ({$completedCount}/{$totalCount} locales completed)"; + } + + echo "\n"; + + if ($status === 'COMPLETED') { + $completed = true; + echo " ✓ Translation completed!\n\n"; + break; + } elseif ($status === 'FAILED') { + echo " ✗ Translation failed\n"; + print_r($progress); + exit(1); + } elseif ($status === 'CANCELLED') { + echo " ✗ Translation was cancelled\n"; + exit(1); + } + + if ($i < $maxAttempts - 1) { + sleep($pollInterval); + } + } + + if (!$completed) { + echo " ⚠ Translation not completed within expected time. Continuing anyway...\n\n"; + } + + // Step 5: Download translated files + echo "5. Downloading translated files...\n"; + $targetLocales = ['es', 'fr', 'de']; + + foreach ($targetLocales as $locale) { + try { + $translatedContent = $api->downloadTranslatedFile($fileUid, $mtUid, $locale); + $outputPath = "/tmp/translated-{$locale}-{$fileName}"; + file_put_contents($outputPath, $translatedContent); + echo " ✓ Downloaded {$locale}: {$outputPath}\n"; + + // Show first 100 chars of content + $preview = substr($translatedContent, 0, 100); + if (strlen($translatedContent) > 100) { + $preview .= '...'; + } + echo " Preview: {$preview}\n"; + } catch (Exception $e) { + echo " ✗ Failed to download {$locale}: {$e->getMessage()}\n"; + } + } + echo "\n"; + + // Step 6: Download all translations as ZIP + echo "6. Downloading all translations as ZIP...\n"; + try { + $zipContent = $api->downloadAllTranslationsZip($fileUid, $mtUid); + $zipPath = "/tmp/all-translations-{$fileUid}.zip"; + file_put_contents($zipPath, $zipContent); + echo " ✓ Downloaded ZIP: {$zipPath}\n"; + echo " Size: " . strlen($zipContent) . " bytes\n\n"; + } catch (Exception $e) { + echo " ✗ Failed to download ZIP: {$e->getMessage()}\n\n"; + } + + // Optional: Demonstrate cancellation with a new translation + echo "7. (Optional) Demonstrating translation cancellation...\n"; + echo " Uploading another file...\n"; + $uploadResult2 = $api->uploadFile($filePath, "cancel-demo-{$fileName}", $fileType); + $fileUid2 = $uploadResult2['fileUid']; + + echo " Starting translation to many locales...\n"; + $translateParams2 = new TranslateFileParameters(); + $translateParams2 + ->setSourceLocaleId('en') + ->setTargetLocaleIds(['es', 'fr', 'de', 'it', 'pt', 'ja', 'zh', 'ru']); + + $translateResult2 = $api->translateFile($fileUid2, $translateParams2); + $mtUid2 = $translateResult2['mtUid']; + + echo " Cancelling translation...\n"; + $api->cancelFileTranslation($fileUid2, $mtUid2); + echo " ✓ Cancellation request sent\n"; + + sleep(2); + $progress = $api->getTranslationProgress($fileUid2, $mtUid2); + echo " Final status: {$progress['state']}\n\n"; + + echo "=== Example completed successfully ===\n"; + +} catch (Exception $e) { + echo "\nError: {$e->getMessage()}\n"; + if (method_exists($e, 'getTraceAsString')) { + echo $e->getTraceAsString() . "\n"; + } + exit(1); +} diff --git a/src/FileTranslations/FileTranslationsApi.php b/src/FileTranslations/FileTranslationsApi.php new file mode 100644 index 0000000..2beb109 --- /dev/null +++ b/src/FileTranslations/FileTranslationsApi.php @@ -0,0 +1,237 @@ +accountUid = $accountUid; + } + + /** + * Factory method to create FileTranslationsApi instance. + * + * @param AuthApiInterface $authProvider + * Authentication provider + * @param string $accountUid + * Account UID in Smartling dashboard + * @param LoggerInterface $logger + * Logger instance + * + * @return FileTranslationsApi + */ + public static function create(AuthApiInterface $authProvider, $accountUid, $logger = null) + { + $client = self::initializeHttpClient(self::ENDPOINT_URL); + + $instance = new self($accountUid, $client, $logger, self::ENDPOINT_URL); + $instance->setAuth($authProvider); + + return $instance; + } + + /** + * {@inheritdoc} + * + * Overrides base implementation to handle file upload with request JSON part. + */ + protected function processBodyOptions($requestData = []) + { + $opts = parent::processBodyOptions($requestData); + + if (!empty($opts['multipart'])) { + foreach ($opts['multipart'] as &$data) { + if ($data['name'] === 'file') { + $data['contents'] = $this->readFile($data['contents']); + if (array_key_exists('filename', $opts)) { + $data['filename'] = $opts['filename']; + } + } elseif ($data['name'] === 'request') { + // Set Content-Type for the request JSON part + $data['headers'] = ['Content-Type' => 'application/json']; + } + } + } + + return $opts; + } + + + /** + * Uploads a file for machine translation. + * + * @param string $realPath + * Real path to the file to read in into stream. + * @param string $fileName + * Logical filename for the file. + * @param string $fileType + * File type identifier (json, xml, html, etc.) + * + * @return array + * Response data containing fileUid + * + * @throws SmartlingApiException + */ + public function uploadFile($realPath, $fileName, $fileType) + { + // Build request JSON object + $requestJson = [ + 'fileType' => $fileType, + ]; + + // Build multipart request + $multipartParams = [ + 'file' => $realPath, + 'request' => json_encode($requestJson), + ]; + + $requestData = $this->getDefaultRequestData('multipart', $multipartParams); + $requestData['filename'] = $fileName; + + return $this->sendRequest('files', $requestData, self::HTTP_METHOD_POST); + } + + /** + * Initiates machine translation for an uploaded file. + * + * @param string $fileUid + * File UID returned from uploadFile + * @param TranslateFileParameters $params + * Translation parameters (source locale, target locales, callback URL) + * + * @return array + * Response data containing mtUid (machine translation UID) + * + * @throws SmartlingApiException + */ + public function translateFile($fileUid, TranslateFileParameters $params) + { + $requestParams = $params->exportToArray(); + + $requestData = $this->getDefaultRequestData('json', $requestParams); + + return $this->sendRequest("files/{$fileUid}/mt", $requestData, self::HTTP_METHOD_POST); + } + + /** + * Gets the translation progress/status for a machine translation job. + * + * @param string $fileUid + * File UID + * @param string $mtUid + * Machine translation UID returned from translateFile + * + * @return array + * Response data containing status (IN_PROGRESS, COMPLETED, FAILED, CANCELLED) + * + * @throws SmartlingApiException + */ + public function getTranslationProgress($fileUid, $mtUid) + { + $requestData = $this->getDefaultRequestData('query', []); + + return $this->sendRequest("files/{$fileUid}/mt/{$mtUid}/status", $requestData, self::HTTP_METHOD_GET); + } + + /** + * Downloads a translated file for a specific locale. + * + * @param string $fileUid + * File UID + * @param string $mtUid + * Machine translation UID + * @param string $localeId + * Target locale ID (e.g., 'es', 'fr', 'de') + * + * @return string + * Raw file content + * + * @throws SmartlingApiException + */ + public function downloadTranslatedFile($fileUid, $mtUid, $localeId) + { + $requestData = $this->getDefaultRequestData('query', []); + unset($requestData['headers']['Accept']); + + return $this->sendRequest("files/{$fileUid}/mt/{$mtUid}/locales/{$localeId}/file", $requestData, self::HTTP_METHOD_GET, true); + } + + /** + * Downloads all translations as a ZIP archive. + * + * @param string $fileUid + * File UID + * @param string $mtUid + * Machine translation UID + * + * @return string + * Raw ZIP file content + * + * @throws SmartlingApiException + */ + public function downloadAllTranslationsZip($fileUid, $mtUid) + { + $requestData = $this->getDefaultRequestData('query', []); + unset($requestData['headers']['Accept']); + + return $this->sendRequest("files/{$fileUid}/mt/{$mtUid}/locales/all/file/zip", $requestData, self::HTTP_METHOD_GET, true); + } + + /** + * Cancels an in-progress machine translation job. + * + * @param string $fileUid + * File UID + * @param string $mtUid + * Machine translation UID + * + * @return bool + * True on success + * + * @throws SmartlingApiException + */ + public function cancelFileTranslation($fileUid, $mtUid) + { + $requestData = $this->getDefaultRequestData('json', []); + + return $this->sendRequest("files/{$fileUid}/mt/{$mtUid}/cancel", $requestData, self::HTTP_METHOD_POST); + } +} diff --git a/src/FileTranslations/Params/TranslateFileParameters.php b/src/FileTranslations/Params/TranslateFileParameters.php new file mode 100644 index 0000000..1de7dce --- /dev/null +++ b/src/FileTranslations/Params/TranslateFileParameters.php @@ -0,0 +1,60 @@ +set('sourceLocaleId', $sourceLocaleId); + + return $this; + } + + /** + * Set target locale IDs. + * + * @param array $targetLocaleIds + * Array of target language codes (e.g., ['es', 'fr', 'de']) + * + * @return TranslateFileParameters + */ + public function setTargetLocaleIds(array $targetLocaleIds) + { + $this->set('targetLocaleIds', $targetLocaleIds); + + return $this; + } + + /** + * Set callback URL for completion notification. + * + * @param string $callbackUrl + * Webhook URL to be called when translation completes + * + * @return TranslateFileParameters + */ + public function setCallbackUrl($callbackUrl) + { + $this->set('callbackUrl', $callbackUrl); + + return $this; + } +} diff --git a/tests/functional/FileTranslationsApiFunctionalTest.php b/tests/functional/FileTranslationsApiFunctionalTest.php new file mode 100644 index 0000000..efc2105 --- /dev/null +++ b/tests/functional/FileTranslationsApiFunctionalTest.php @@ -0,0 +1,176 @@ + user_id= user_key= ./vendor/bin/phpunit tests/functional/FileTranslationsApiFunctionalTest.php + */ +class FileTranslationsApiFunctionalTest extends TestCase +{ + /** + * @var FileTranslationsApi + */ + private $api; + + /** + * @var string + */ + private $testFilePath; + + /** + * Sets up the test environment. + */ + protected function setUp(): void + { + $accountUid = getenv('account_uid'); + $userId = getenv('user_id'); + $userKey = getenv('user_key'); + + if (empty($accountUid) || empty($userId) || empty($userKey)) { + $this->markTestSkipped( + 'Required environment variables not set: account_uid, user_id, user_key' + ); + } + + $authProvider = AuthTokenProvider::create($userId, $userKey); + $this->api = FileTranslationsApi::create($authProvider, $accountUid); + + $this->testFilePath = __DIR__ . '/../resources/test-fts.json'; + + if (!file_exists($this->testFilePath)) { + $this->markTestSkipped('Test file not found: ' . $this->testFilePath); + } + } + + /** + * Tests the complete file translation workflow: + * 1. Upload file + * 2. Initiate translation + * 3. Poll translation progress + * 4. Download translated file + * + * @covers \Smartling\FileTranslations\FileTranslationsApi::uploadFile + * @covers \Smartling\FileTranslations\FileTranslationsApi::translateFile + * @covers \Smartling\FileTranslations\FileTranslationsApi::getTranslationProgress + * @covers \Smartling\FileTranslations\FileTranslationsApi::downloadTranslatedFile + */ + public function testCompleteTranslationWorkflow() + { + // Step 1: Upload file + $uploadResult = $this->api->uploadFile( + $this->testFilePath, + 'test-fts-' . time() . '.json', + 'json' + ); + + $this->assertIsArray($uploadResult); + $this->assertArrayHasKey('fileUid', $uploadResult); + $fileUid = $uploadResult['fileUid']; + + // Step 2: Initiate translation + $translateParams = new TranslateFileParameters(); + $translateParams->setSourceLocaleId('en') + ->setTargetLocaleIds(['es']); + + $translateResult = $this->api->translateFile($fileUid, $translateParams); + + $this->assertIsArray($translateResult); + $this->assertArrayHasKey('mtUid', $translateResult); + $mtUid = $translateResult['mtUid']; + + // Step 3: Poll translation progress + $maxAttempts = 30; // Maximum number of polling attempts + $pollInterval = 2; // Seconds between polls + $completed = false; + + for ($i = 0; $i < $maxAttempts; $i++) { + sleep($pollInterval); + + $progress = $this->api->getTranslationProgress($fileUid, $mtUid); + + $this->assertIsArray($progress); + $this->assertArrayHasKey('state', $progress); + + $status = $progress['state']; + + if ($status === 'COMPLETED') { + $completed = true; + break; + } elseif ($status === 'FAILED') { + $this->fail('Translation failed: ' . json_encode($progress)); + } elseif ($status === 'CANCELLED') { + $this->fail('Translation was cancelled'); + } + + // Status should be IN_PROGRESS + $this->assertEquals('IN_PROGRESS', $status); + } + + if (!$completed) { + $this->markTestIncomplete('Translation did not complete within expected time'); + } + + // Step 4: Download translated file + $translatedContent = $this->api->downloadTranslatedFile($fileUid, $mtUid, 'es'); + + $this->assertNotEmpty($translatedContent); + + // Verify it's valid JSON + $translatedData = json_decode($translatedContent, true); + $this->assertIsArray($translatedData); + $this->assertNotNull($translatedData); + } + + /** + * Tests cancelling a translation. + * + * @covers \Smartling\FileTranslations\FileTranslationsApi::cancelFileTranslation + */ + public function testCancelFileTranslation() + { + // Upload file + $uploadResult = $this->api->uploadFile( + $this->testFilePath, + 'test-cancel-' . time() . '.json', + 'json' + ); + + $fileUid = $uploadResult['fileUid']; + + // Initiate translation with multiple locales to ensure it takes some time + $translateParams = new TranslateFileParameters(); + $translateParams->setSourceLocaleId('en') + ->setTargetLocaleIds(['es', 'fr', 'de', 'it', 'pt']); + + $translateResult = $this->api->translateFile($fileUid, $translateParams); + $mtUid = $translateResult['mtUid']; + + // Immediately cancel the translation + $cancelResult = $this->api->cancelFileTranslation($fileUid, $mtUid); + + $this->assertTrue($cancelResult); + + // Verify the translation was cancelled + sleep(2); // Give the system time to process the cancellation + + $progress = $this->api->getTranslationProgress($fileUid, $mtUid); + $this->assertIsArray($progress); + $this->assertArrayHasKey('state', $progress); + + // Status should be CANCELLED. + $this->assertEquals('CANCELED', $progress['state']); + } +} diff --git a/tests/resources/test-fts.json b/tests/resources/test-fts.json new file mode 100644 index 0000000..048b833 --- /dev/null +++ b/tests/resources/test-fts.json @@ -0,0 +1,5 @@ +{ + "hello": "Hello, world!", + "goodbye": "Goodbye!", + "welcome": "Welcome to our application" +} diff --git a/tests/unit/FileTranslationsApiTest.php b/tests/unit/FileTranslationsApiTest.php new file mode 100644 index 0000000..5876866 --- /dev/null +++ b/tests/unit/FileTranslationsApiTest.php @@ -0,0 +1,316 @@ +object = $this->getMockBuilder('Smartling\FileTranslations\FileTranslationsApi') + ->setMethods(['readFile']) + ->setConstructorArgs([ + $this->accountUid, + $this->client, + null, + FileTranslationsApi::ENDPOINT_URL, + ]) + ->getMock(); + + $this->object->expects($this->any()) + ->method('readFile') + ->willReturn($this->streamPlaceholder); + + $this->invokeMethod( + $this->object, + 'setAuth', + [ + $this->authProvider + ] + ); + } + + /** + * Sets up the fixture, for example, opens a network connection. + * This method is called before a test is executed. + */ + protected function setUp(): void + { + $this->prepareHttpClientMock(); + $this->prepareAuthProviderMock(); + $this->prepareFileTranslationsApiMock(); + } + + /** + * Tests constructor. + * + * @param string $accountUid + * Account UID string. + * @param \GuzzleHttp\ClientInterface $client + * Mock of Guzzle http client instance. + * @param string|null $expected_base_url + * Base Url string that will be used as based url. + * + * @covers \Smartling\FileTranslations\FileTranslationsApi::__construct + * + * @dataProvider constructorDataProvider + */ + public function testConstructor($accountUid, $client, $expected_base_url) + { + $this->prepareClientResponseMock(); + $api = new FileTranslationsApi($accountUid, $client, null, $expected_base_url); + + $this->assertEquals(\rtrim($expected_base_url, '/') . '/' . $accountUid, + $this->invokeMethod($api, 'getBaseUrl')); + $this->assertEquals($client, $this->invokeMethod($api, 'getHttpClient')); + } + + /** + * Data provider for testConstructor method. + * + * @return array + */ + public function constructorDataProvider() + { + $this->prepareHttpClientMock(); + + $mockedClient = $this->client; + + return [ + ['account-uid-123', $mockedClient, FileTranslationsApi::ENDPOINT_URL], + ['account-uid-456', $mockedClient, FileTranslationsApi::ENDPOINT_URL . '/'], + ]; + } + + /** + * @covers \Smartling\FileTranslations\FileTranslationsApi::uploadFile + */ + public function testUploadFile() + { + $this->prepareClientResponseMock(); + + $this->client + ->expects($this->once()) + ->method('request') + ->willReturnCallback(function(string $method, string $uri, array $options) { + $this->assertEquals('post', $method); + $this->assertEquals(FileTranslationsApi::ENDPOINT_URL . '/' . $this->accountUid . '/files', $uri); + + // Verify headers + $this->assertEquals('application/json', $options['headers']['Accept']); + $this->assertStringStartsWith('Bearer', $options['headers']['Authorization']); + + // Verify multipart structure + $this->assertArrayHasKey('multipart', $options); + $this->assertIsArray($options['multipart']); + + // Find file and request parts + $filePart = null; + $requestPart = null; + foreach ($options['multipart'] as $part) { + if ($part['name'] === 'file') { + $filePart = $part; + } + if ($part['name'] === 'request') { + $requestPart = $part; + } + } + + $this->assertNotNull($filePart, 'File part should be present'); + $this->assertNotNull($requestPart, 'Request part should be present'); + + // Verify file part + $this->assertEquals($this->streamPlaceholder, $filePart['contents']); + $this->assertEquals('test.json', $filePart['filename']); + + // Verify request part + $requestData = json_decode($requestPart['contents'], true); + $this->assertEquals('json', $requestData['fileType']); + + return $this->responseMock; + }); + + $this->object->uploadFile('tests/resources/test-fts.json', 'test.json', 'json'); + } + + /** + * @covers \Smartling\FileTranslations\FileTranslationsApi::translateFile + */ + public function testTranslateFile() + { + $this->prepareClientResponseMock(); + + $fileUid = 'file-uid-123'; + + $this->client + ->expects($this->once()) + ->method('request') + ->willReturnCallback(function(string $method, string $uri, array $options) use ($fileUid) { + $this->assertEquals('post', $method); + $this->assertEquals( + FileTranslationsApi::ENDPOINT_URL . '/' . $this->accountUid . '/files/' . $fileUid . '/mt', + $uri + ); + + // Verify JSON body + $this->assertArrayHasKey('json', $options); + $this->assertEquals('en', $options['json']['sourceLocaleId']); + $this->assertEquals(['es', 'fr', 'de'], $options['json']['targetLocaleIds']); + $this->assertEquals('https://example.com/callback', $options['json']['callbackUrl']); + + return $this->responseMock; + }); + + $params = new TranslateFileParameters(); + $params->setSourceLocaleId('en') + ->setTargetLocaleIds(['es', 'fr', 'de']) + ->setCallbackUrl('https://example.com/callback'); + + $this->object->translateFile($fileUid, $params); + } + + /** + * @covers \Smartling\FileTranslations\FileTranslationsApi::getTranslationProgress + */ + public function testGetTranslationProgress() + { + $this->prepareClientResponseMock(); + + $fileUid = 'file-uid-123'; + $mtUid = 'mt-uid-456'; + + $this->client + ->expects($this->once()) + ->method('request') + ->willReturnCallback(function(string $method, string $uri, array $options) use ($fileUid, $mtUid) { + $this->assertEquals('get', $method); + $this->assertEquals( + FileTranslationsApi::ENDPOINT_URL . '/' . $this->accountUid . '/files/' . $fileUid . '/mt/' . $mtUid . '/status', + $uri + ); + + return $this->responseMock; + }); + + $this->object->getTranslationProgress($fileUid, $mtUid); + } + + /** + * @covers \Smartling\FileTranslations\FileTranslationsApi::downloadTranslatedFile + */ + public function testDownloadTranslatedFile() + { + // Create a mock response with raw content + $rawContent = '{"translated": "content"}'; + $stream = $this->createMock(\Psr\Http\Message\StreamInterface::class); + $stream->method('__toString')->willReturn($rawContent); + + $responseMock = $this->createMock(\GuzzleHttp\Psr7\Response::class); + $responseMock->method('getBody')->willReturn($stream); + $responseMock->method('getStatusCode')->willReturn(200); + + $fileUid = 'file-uid-123'; + $mtUid = 'mt-uid-456'; + $localeId = 'es'; + + $this->client + ->expects($this->once()) + ->method('request') + ->willReturnCallback(function(string $method, string $uri, array $options) use ($fileUid, $mtUid, $localeId, $responseMock) { + $this->assertEquals('get', $method); + $this->assertEquals( + FileTranslationsApi::ENDPOINT_URL . '/' . $this->accountUid . '/files/' . $fileUid . '/mt/' . $mtUid . '/locales/' . $localeId . '/file', + $uri + ); + + // Verify Accept header is removed for raw download + $this->assertArrayNotHasKey('Accept', $options['headers']); + + return $responseMock; + }); + + $result = $this->object->downloadTranslatedFile($fileUid, $mtUid, $localeId); + $this->assertEquals($rawContent, $result); + } + + /** + * @covers \Smartling\FileTranslations\FileTranslationsApi::downloadAllTranslationsZip + */ + public function testDownloadAllTranslationsZip() + { + // Create a mock response with raw content + $rawContent = 'ZIP_BINARY_CONTENT'; + $stream = $this->createMock(\Psr\Http\Message\StreamInterface::class); + $stream->method('__toString')->willReturn($rawContent); + + $responseMock = $this->createMock(\GuzzleHttp\Psr7\Response::class); + $responseMock->method('getBody')->willReturn($stream); + $responseMock->method('getStatusCode')->willReturn(200); + + $fileUid = 'file-uid-123'; + $mtUid = 'mt-uid-456'; + + $this->client + ->expects($this->once()) + ->method('request') + ->willReturnCallback(function(string $method, string $uri, array $options) use ($fileUid, $mtUid, $responseMock) { + $this->assertEquals('get', $method); + $this->assertEquals( + FileTranslationsApi::ENDPOINT_URL . '/' . $this->accountUid . '/files/' . $fileUid . '/mt/' . $mtUid . '/locales/all/file/zip', + $uri + ); + + // Verify Accept header is removed for raw download + $this->assertArrayNotHasKey('Accept', $options['headers']); + + return $responseMock; + }); + + $result = $this->object->downloadAllTranslationsZip($fileUid, $mtUid); + $this->assertEquals($rawContent, $result); + } + + /** + * @covers \Smartling\FileTranslations\FileTranslationsApi::cancelFileTranslation + */ + public function testCancelFileTranslation() + { + $this->prepareClientResponseMock(); + + $fileUid = 'file-uid-123'; + $mtUid = 'mt-uid-456'; + + $this->client + ->expects($this->once()) + ->method('request') + ->willReturnCallback(function(string $method, string $uri, array $options) use ($fileUid, $mtUid) { + $this->assertEquals('post', $method); + $this->assertEquals( + FileTranslationsApi::ENDPOINT_URL . '/' . $this->accountUid . '/files/' . $fileUid . '/mt/' . $mtUid . '/cancel', + $uri + ); + + // Verify it's a JSON request + $this->assertArrayHasKey('json', $options); + + return $this->responseMock; + }); + + $result = $this->object->cancelFileTranslation($fileUid, $mtUid); + // Result can be either true (empty data) or an array (with data) + $this->assertTrue($result === true || is_array($result)); + } +}