Skip to content

Commit ebf56d4

Browse files
authored
Merge pull request #147 from Smartling/CON-1998-add-fts-api-client
Add FileTranslations API client (CON-1998)
2 parents 7cddf60 + 487f0e3 commit ebf56d4

7 files changed

Lines changed: 1125 additions & 0 deletions

File tree

CLAUDE.md

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Repository Overview
6+
7+
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.
8+
9+
## Development Commands
10+
11+
### Install Dependencies
12+
```bash
13+
composer install
14+
```
15+
16+
### Run All Tests
17+
Tests require Smartling API credentials as environment variables:
18+
```bash
19+
account_uid=<account_uid> project_id=<project_id> user_id=<user_id> user_key=<user_key> ./vendor/bin/phpunit
20+
```
21+
22+
### Run Specific Test Suites
23+
```bash
24+
# Unit tests only
25+
./vendor/bin/phpunit --testsuite unit
26+
27+
# Functional tests only (requires API credentials)
28+
account_uid=<account_uid> project_id=<project_id> user_id=<user_id> user_key=<user_key> ./vendor/bin/phpunit --testsuite functional
29+
```
30+
31+
### Run Individual Test Files
32+
```bash
33+
./vendor/bin/phpunit tests/unit/SomeTest.php
34+
```
35+
36+
## Architecture
37+
38+
### Core Components
39+
40+
**BaseApiAbstract** (`src/BaseApiAbstract.php`)
41+
- Abstract base class for all API clients
42+
- Handles HTTP communication via Guzzle
43+
- Manages authentication via `AuthApiInterface`
44+
- Implements automatic token refresh on 401 errors
45+
- Provides logging support via PSR-3 `LoggerInterface`
46+
- Standardizes error handling and response processing
47+
- All API classes extend this base
48+
49+
**Authentication Flow**
50+
- `AuthTokenProvider` (`src/AuthApi/`) implements `AuthApiInterface`
51+
- Manages OAuth access tokens and refresh tokens with automatic expiration handling
52+
- Token refresh happens automatically when expired or on 401 responses
53+
- All API clients receive an auth provider instance and use it to authenticate requests
54+
55+
**API Client Pattern**
56+
Each Smartling API endpoint has a dedicated API class:
57+
- `FileApi` - File upload, download, and management
58+
- `JobsApi` - Translation job management
59+
- `BatchApi` / `BatchApiV2` - Batch operations
60+
- `ContextApi` - Visual context and screenshots
61+
- `ProjectApi` - Project information
62+
- `AuditLogApi` - Audit logs
63+
- `ProgressTrackerApi` - Translation progress tracking
64+
- `TranslationRequestsApi` - Translation request management
65+
- `DistributedLockServiceApi` - Distributed locking
66+
67+
**Parameter Objects**
68+
- Located in `*/Params/` directories under each API module
69+
- Implement `ParameterInterface` or extend `BaseParameters`
70+
- Provide type-safe parameter building for API methods
71+
- Example: `UploadFileParameters`, `CreateJobParameters`, `DownloadFileParameters`
72+
73+
### Directory Structure
74+
75+
```
76+
src/
77+
├── BaseApiAbstract.php # Base class for all API clients
78+
├── AuthApi/ # Authentication provider
79+
├── File/ # File API (upload/download)
80+
├── Jobs/ # Jobs API
81+
├── Batch/ # Batch operations (v1 and v2)
82+
├── Context/ # Visual context API
83+
├── Project/ # Project information API
84+
├── AuditLog/ # Audit logging API
85+
├── ProgressTracker/ # Progress tracking API
86+
├── TranslationRequests/ # Translation requests API
87+
├── DistributedLockService/ # Distributed lock service
88+
├── Parameters/ # Base parameter classes
89+
└── Exceptions/ # Custom exceptions
90+
91+
tests/
92+
├── unit/ # Unit tests (no API calls)
93+
└── functional/ # Functional tests (require API credentials)
94+
95+
examples/ # Usage examples for each API
96+
```
97+
98+
### API Client Instantiation Pattern
99+
100+
All API clients follow this factory pattern:
101+
```php
102+
$authProvider = AuthTokenProvider::create($userIdentifier, $userSecretKey);
103+
$api = SomeApi::create($authProvider, $projectId, $logger);
104+
```
105+
106+
Each API class:
107+
1. Defines its own `ENDPOINT_URL` constant
108+
2. Provides a static `create()` factory method
109+
3. Initializes an HTTP client via `BaseApiAbstract::initializeHttpClient()`
110+
4. Sets the auth provider via `setAuth()`
111+
112+
### Error Handling
113+
114+
- `SmartlingApiException` is thrown for all API errors
115+
- Errors contain structured error information from the API response
116+
- 401 errors trigger automatic token refresh and retry
117+
- Sensitive data (tokens, credentials) is redacted from logs
118+
119+
### Logging
120+
121+
- All API clients accept an optional PSR-3 `LoggerInterface`
122+
- Requests and responses are logged for debugging
123+
- Sensitive data is automatically redacted in logs
124+
125+
## PHP Version Support
126+
127+
- PHP 7.3+ and PHP 8.x
128+
- Uses PSR-4 autoloading
129+
- Requires Guzzle 6.x or 7.x for HTTP client
130+
- Requires PSR Log 1.x or 3.x for logging
131+
132+
## Testing Strategy
133+
134+
- **Unit tests** (`tests/unit/`) - Test logic without making actual API calls
135+
- **Functional tests** (`tests/functional/`) - Integration tests that call the real Smartling API
136+
- Functional tests require valid Smartling credentials passed as environment variables
137+
- Test resources are stored in `tests/resources/`
138+
139+
## Examples
140+
141+
The `examples/` directory contains working examples for each API:
142+
- Run with: `php file-example.php --project-id=XXX --user-id=XXX --secret-key=XXX`
143+
- Each example demonstrates common operations for that API
144+
- Examples are self-contained and include inline documentation
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
<?php
2+
3+
/**
4+
* File Translations API Example
5+
*
6+
* This example demonstrates how to use the File Translations API to:
7+
* 1. Upload a file for machine translation
8+
* 2. Initiate translation to multiple target locales
9+
* 3. Poll translation progress
10+
* 4. Download translated files
11+
* 5. Download all translations as ZIP
12+
* 6. Cancel a translation (optional)
13+
*
14+
* Usage:
15+
* php file-translations-example.php --account-uid=XXX --user-id=XXX --secret-key=XXX [--file-path=path/to/file.json]
16+
*/
17+
18+
require_once '../vendor/autoload.php';
19+
20+
use Smartling\AuthApi\AuthTokenProvider;
21+
use Smartling\FileTranslations\FileTranslationsApi;
22+
use Smartling\FileTranslations\Params\TranslateFileParameters;
23+
24+
// Parse command line arguments
25+
$options = getopt('', [
26+
'account-uid:',
27+
'user-id:',
28+
'secret-key:',
29+
'file-path::',
30+
]);
31+
32+
if (!isset($options['account-uid']) || !isset($options['user-id']) || !isset($options['secret-key'])) {
33+
echo "Usage: php file-translations-example.php --account-uid=XXX --user-id=XXX --secret-key=XXX [--file-path=path/to/file.json]\n";
34+
exit(1);
35+
}
36+
37+
$accountUid = $options['account-uid'];
38+
$userId = $options['user-id'];
39+
$secretKey = $options['secret-key'];
40+
$filePath = $options['file-path'] ?? __DIR__ . '/../tests/resources/test-fts.json';
41+
42+
// Verify file exists
43+
if (!file_exists($filePath)) {
44+
echo "Error: File not found: {$filePath}\n";
45+
exit(1);
46+
}
47+
48+
try {
49+
echo "=== File Translations API Example ===\n\n";
50+
51+
// Step 1: Initialize API client
52+
echo "1. Initializing API client...\n";
53+
$authProvider = AuthTokenProvider::create($userId, $secretKey);
54+
$api = FileTranslationsApi::create($authProvider, $accountUid);
55+
echo " ✓ API client initialized\n\n";
56+
57+
// Step 2: Upload file
58+
echo "2. Uploading file: {$filePath}\n";
59+
$fileName = basename($filePath);
60+
$fileType = pathinfo($filePath, PATHINFO_EXTENSION);
61+
62+
$uploadResult = $api->uploadFile($filePath, $fileName, $fileType);
63+
$fileUid = $uploadResult['fileUid'];
64+
echo " ✓ File uploaded successfully\n";
65+
echo " File UID: {$fileUid}\n\n";
66+
67+
// Step 3: Initiate translation
68+
echo "3. Initiating translation to Spanish, French, and German...\n";
69+
$translateParams = new TranslateFileParameters();
70+
$translateParams
71+
->setSourceLocaleId('en')
72+
->setTargetLocaleIds(['es', 'fr', 'de']);
73+
74+
$translateResult = $api->translateFile($fileUid, $translateParams);
75+
$mtUid = $translateResult['mtUid'];
76+
echo " ✓ Translation initiated\n";
77+
echo " MT UID: {$mtUid}\n\n";
78+
79+
// Step 4: Poll translation progress
80+
echo "4. Polling translation progress...\n";
81+
$maxAttempts = 60;
82+
$pollInterval = 5; // seconds
83+
$completed = false;
84+
85+
for ($i = 0; $i < $maxAttempts; $i++) {
86+
$progress = $api->getTranslationProgress($fileUid, $mtUid);
87+
$status = $progress['state'];
88+
89+
echo " Status: {$status}";
90+
91+
if (isset($progress['completedLocales'])) {
92+
$completedCount = count($progress['completedLocales']);
93+
$totalCount = count($translateParams->exportToArray()['targetLocaleIds']);
94+
echo " ({$completedCount}/{$totalCount} locales completed)";
95+
}
96+
97+
echo "\n";
98+
99+
if ($status === 'COMPLETED') {
100+
$completed = true;
101+
echo " ✓ Translation completed!\n\n";
102+
break;
103+
} elseif ($status === 'FAILED') {
104+
echo " ✗ Translation failed\n";
105+
print_r($progress);
106+
exit(1);
107+
} elseif ($status === 'CANCELLED') {
108+
echo " ✗ Translation was cancelled\n";
109+
exit(1);
110+
}
111+
112+
if ($i < $maxAttempts - 1) {
113+
sleep($pollInterval);
114+
}
115+
}
116+
117+
if (!$completed) {
118+
echo " ⚠ Translation not completed within expected time. Continuing anyway...\n\n";
119+
}
120+
121+
// Step 5: Download translated files
122+
echo "5. Downloading translated files...\n";
123+
$targetLocales = ['es', 'fr', 'de'];
124+
125+
foreach ($targetLocales as $locale) {
126+
try {
127+
$translatedContent = $api->downloadTranslatedFile($fileUid, $mtUid, $locale);
128+
$outputPath = "/tmp/translated-{$locale}-{$fileName}";
129+
file_put_contents($outputPath, $translatedContent);
130+
echo " ✓ Downloaded {$locale}: {$outputPath}\n";
131+
132+
// Show first 100 chars of content
133+
$preview = substr($translatedContent, 0, 100);
134+
if (strlen($translatedContent) > 100) {
135+
$preview .= '...';
136+
}
137+
echo " Preview: {$preview}\n";
138+
} catch (Exception $e) {
139+
echo " ✗ Failed to download {$locale}: {$e->getMessage()}\n";
140+
}
141+
}
142+
echo "\n";
143+
144+
// Step 6: Download all translations as ZIP
145+
echo "6. Downloading all translations as ZIP...\n";
146+
try {
147+
$zipContent = $api->downloadAllTranslationsZip($fileUid, $mtUid);
148+
$zipPath = "/tmp/all-translations-{$fileUid}.zip";
149+
file_put_contents($zipPath, $zipContent);
150+
echo " ✓ Downloaded ZIP: {$zipPath}\n";
151+
echo " Size: " . strlen($zipContent) . " bytes\n\n";
152+
} catch (Exception $e) {
153+
echo " ✗ Failed to download ZIP: {$e->getMessage()}\n\n";
154+
}
155+
156+
// Optional: Demonstrate cancellation with a new translation
157+
echo "7. (Optional) Demonstrating translation cancellation...\n";
158+
echo " Uploading another file...\n";
159+
$uploadResult2 = $api->uploadFile($filePath, "cancel-demo-{$fileName}", $fileType);
160+
$fileUid2 = $uploadResult2['fileUid'];
161+
162+
echo " Starting translation to many locales...\n";
163+
$translateParams2 = new TranslateFileParameters();
164+
$translateParams2
165+
->setSourceLocaleId('en')
166+
->setTargetLocaleIds(['es', 'fr', 'de', 'it', 'pt', 'ja', 'zh', 'ru']);
167+
168+
$translateResult2 = $api->translateFile($fileUid2, $translateParams2);
169+
$mtUid2 = $translateResult2['mtUid'];
170+
171+
echo " Cancelling translation...\n";
172+
$api->cancelFileTranslation($fileUid2, $mtUid2);
173+
echo " ✓ Cancellation request sent\n";
174+
175+
sleep(2);
176+
$progress = $api->getTranslationProgress($fileUid2, $mtUid2);
177+
echo " Final status: {$progress['state']}\n\n";
178+
179+
echo "=== Example completed successfully ===\n";
180+
181+
} catch (Exception $e) {
182+
echo "\nError: {$e->getMessage()}\n";
183+
if (method_exists($e, 'getTraceAsString')) {
184+
echo $e->getTraceAsString() . "\n";
185+
}
186+
exit(1);
187+
}

0 commit comments

Comments
 (0)