From 296517b72001dd5f4228b4905586e4b849088a9c Mon Sep 17 00:00:00 2001 From: Bohdan Bobko <172895427+bhdnb@users.noreply.github.com> Date: Wed, 30 Sep 2026 16:14:21 +0300 Subject: [PATCH 1/9] feat: add Enterprise report status, download and settings templates --- .../Api/Enterprise/GroupApi.php | 27 +++ .../Api/Enterprise/ReportApi.php | 153 +++++++++++++ src/CrowdinApiClient/Api/ReportApi.php | 2 +- .../Model/ReportSettingsTemplate.php | 28 +++ .../Api/Enterprise/GroupApiTest.php | 48 ++++ .../Api/Enterprise/ReportApiTest.php | 205 ++++++++++++++++++ tests/CrowdinApiClient/Api/ReportApiTest.php | 28 +++ .../Model/ReportSettingsTemplateTest.php | 24 ++ 8 files changed, 514 insertions(+), 1 deletion(-) diff --git a/src/CrowdinApiClient/Api/Enterprise/GroupApi.php b/src/CrowdinApiClient/Api/Enterprise/GroupApi.php index a2401dd0..c36619d3 100644 --- a/src/CrowdinApiClient/Api/Enterprise/GroupApi.php +++ b/src/CrowdinApiClient/Api/Enterprise/GroupApi.php @@ -4,6 +4,7 @@ use CrowdinApiClient\Api\AbstractApi; use CrowdinApiClient\Http\ResponseDecorator\ResponseModelListDecorator; +use CrowdinApiClient\Model\DownloadFile; use CrowdinApiClient\Model\Enterprise\Group; use CrowdinApiClient\Model\Enterprise\GroupManager; use CrowdinApiClient\Model\Enterprise\GroupTeam; @@ -100,6 +101,32 @@ public function report(int $groupID, array $data): ?Report return $this->_post($path, Report::class, $data); } + /** + * Check Group Report Generation Status + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.groups.reports.get API Documentation + * + * @param int $groupId + * @param string $reportId + * @return Report|null + */ + public function getReport(int $groupId, string $reportId): ?Report + { + return $this->_get(sprintf('groups/%d/reports/%s', $groupId, $reportId), Report::class); + } + + /** + * Download Group Report + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.groups.reports.download.download API Documentation + * + * @param int $groupId + * @param string $reportId + * @return DownloadFile|null + */ + public function downloadReport(int $groupId, string $reportId): ?DownloadFile + { + return $this->_get(sprintf('groups/%d/reports/%s/download', $groupId, $reportId), DownloadFile::class); + } + /** * List Group Managers * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.groups.managers.getMany API Documentation diff --git a/src/CrowdinApiClient/Api/Enterprise/ReportApi.php b/src/CrowdinApiClient/Api/Enterprise/ReportApi.php index 932652cb..8f834dd9 100644 --- a/src/CrowdinApiClient/Api/Enterprise/ReportApi.php +++ b/src/CrowdinApiClient/Api/Enterprise/ReportApi.php @@ -3,7 +3,10 @@ namespace CrowdinApiClient\Api\Enterprise; use CrowdinApiClient\Api\AbstractApi; +use CrowdinApiClient\Model\DownloadFile; use CrowdinApiClient\Model\Report; +use CrowdinApiClient\Model\ReportSettingsTemplate; +use CrowdinApiClient\ModelCollection; /** * Reports help to estimate costs, calculate translation costs, and identify the top members. @@ -28,4 +31,154 @@ public function generate(array $data): ?Report { return $this->_post('reports', Report::class, $data); } + + /** + * Check Organization Report Generation Status + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.reports.get API Documentation + * + * @param string $reportId + * @return Report|null + */ + public function get(string $reportId): ?Report + { + return $this->_get(sprintf('reports/%s', $reportId), Report::class); + } + + /** + * Download Organization Report + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.reports.download.download API Documentation + * + * @param string $reportId + * @return DownloadFile|null + */ + public function download(string $reportId): ?DownloadFile + { + return $this->_get(sprintf('reports/%s/download', $reportId), DownloadFile::class); + } + + /** + * Generate Project Report + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.reports.post API Documentation + * + * @param int $projectId + * @param array $data + * string $data[name]
+ * array $data[schema] + * @return Report|null + */ + public function generateProjectReport(int $projectId, array $data): ?Report + { + return $this->_post(sprintf('projects/%d/reports', $projectId), Report::class, $data); + } + + /** + * Check Project Report Generation Status + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.reports.get API Documentation + * + * @param int $projectId + * @param string $reportId + * @return Report|null + */ + public function getProjectReport(int $projectId, string $reportId): ?Report + { + return $this->_get(sprintf('projects/%d/reports/%s', $projectId, $reportId), Report::class); + } + + /** + * Download Project Report + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.reports.download.download API Documentation + * + * @param int $projectId + * @param string $reportId + * @return DownloadFile|null + */ + public function downloadProjectReport(int $projectId, string $reportId): ?DownloadFile + { + return $this->_get(sprintf('projects/%d/reports/%s/download', $projectId, $reportId), DownloadFile::class); + } + + /** + * List Report Settings Templates + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.reports.settings-templates.getMany API Documentation + * + * @param array $params + * integer $params[projectId]
+ * integer $params[groupId]
+ * integer $params[limit]
+ * integer $params[offset] + * @return ModelCollection + */ + public function listReportSettingsTemplates(array $params = []): ModelCollection + { + return $this->_list('reports/settings-templates', ReportSettingsTemplate::class, $params); + } + + /** + * Get Report Settings Template + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.reports.settings-templates.get API Documentation + * + * @param int $reportSettingsTemplateId + * @return ReportSettingsTemplate|null + */ + public function getReportSettingsTemplate(int $reportSettingsTemplateId): ?ReportSettingsTemplate + { + return $this->_get( + sprintf('reports/settings-templates/%d', $reportSettingsTemplateId), + ReportSettingsTemplate::class + ); + } + + /** + * Add Report Settings Template + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.reports.settings-templates.post API Documentation + * + * @param array $data + * string $data[name] required
+ * string $data[currency] required
+ * string $data[unit] required
+ * array $data[config] required
+ * integer $data[projectId] Can't be used together with groupId
+ * integer $data[groupId] Can't be used together with projectId + * @return ReportSettingsTemplate|null + */ + public function createReportSettingsTemplate(array $data): ?ReportSettingsTemplate + { + $forbiddenKeys = ['id', 'isGlobal', 'createdAt', 'updatedAt']; + + return $this->_create( + 'reports/settings-templates', + ReportSettingsTemplate::class, + array_filter($data, static function ($value, string $key) use ($forbiddenKeys): bool { + return $value !== null && !in_array($key, $forbiddenKeys, true); + }, ARRAY_FILTER_USE_BOTH) + ); + } + + /** + * Edit Report Settings Template + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.reports.settings-templates.patch API Documentation + * + * @param ReportSettingsTemplate $reportSettingsTemplate + * @return ReportSettingsTemplate|null + */ + public function updateReportSettingsTemplate( + ReportSettingsTemplate $reportSettingsTemplate + ): ?ReportSettingsTemplate { + return $this->_update( + sprintf('reports/settings-templates/%d', $reportSettingsTemplate->getId()), + $reportSettingsTemplate + ); + } + + /** + * Delete Report Settings Template + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.reports.settings-templates.delete API Documentation + * + * @param int $reportSettingsTemplateId + * @return mixed + */ + public function deleteReportSettingsTemplate(int $reportSettingsTemplateId) + { + return $this->_delete(sprintf('reports/settings-templates/%d', $reportSettingsTemplateId)); + } } diff --git a/src/CrowdinApiClient/Api/ReportApi.php b/src/CrowdinApiClient/Api/ReportApi.php index 0346e352..b1a5c4b2 100644 --- a/src/CrowdinApiClient/Api/ReportApi.php +++ b/src/CrowdinApiClient/Api/ReportApi.php @@ -85,7 +85,7 @@ public function getReportSettingsTemplate(int $projectId, int $reportSettingsTem */ public function createReportSettingsTemplate(int $projectId, array $data): ?ReportSettingsTemplate { - $forbiddenKeys = ['id', 'createdAt', 'updatedAt']; + $forbiddenKeys = ['id', 'projectId', 'groupId', 'createdAt', 'updatedAt']; $path = sprintf('projects/%d/reports/settings-templates', $projectId); return $this->_post( diff --git a/src/CrowdinApiClient/Model/ReportSettingsTemplate.php b/src/CrowdinApiClient/Model/ReportSettingsTemplate.php index a0076746..4267999c 100644 --- a/src/CrowdinApiClient/Model/ReportSettingsTemplate.php +++ b/src/CrowdinApiClient/Model/ReportSettingsTemplate.php @@ -16,6 +16,16 @@ class ReportSettingsTemplate extends BaseModel */ protected $id; + /** + * @var int|null + */ + protected $projectId; + + /** + * @var int|null + */ + protected $groupId; + /** * @var string */ @@ -61,6 +71,12 @@ public function __construct(array $data = []) parent::__construct($data); $this->id = (int)$this->getDataProperty('id'); + $this->projectId = $this->getDataProperty('projectId') !== null + ? (int)$this->getDataProperty('projectId') + : null; + $this->groupId = $this->getDataProperty('groupId') !== null + ? (int)$this->getDataProperty('groupId') + : null; $this->name = (string)$this->getDataProperty('name'); $this->currency = (string)$this->getDataProperty('currency'); $this->unit = (string)$this->getDataProperty('unit'); @@ -76,6 +92,16 @@ public function getId(): int return $this->id; } + public function getProjectId(): ?int + { + return $this->projectId; + } + + public function getGroupId(): ?int + { + return $this->groupId; + } + public function getName(): string { return $this->name; @@ -163,6 +189,8 @@ public function toArray(): array { return [ 'id' => $this->id, + 'projectId' => $this->projectId, + 'groupId' => $this->groupId, 'name' => $this->name, 'currency' => $this->currency, 'unit' => $this->unit, diff --git a/tests/CrowdinApiClient/Api/Enterprise/GroupApiTest.php b/tests/CrowdinApiClient/Api/Enterprise/GroupApiTest.php index 02c0762b..59134c69 100644 --- a/tests/CrowdinApiClient/Api/Enterprise/GroupApiTest.php +++ b/tests/CrowdinApiClient/Api/Enterprise/GroupApiTest.php @@ -2,6 +2,7 @@ namespace CrowdinApiClient\Tests\Api\Enterprise; +use CrowdinApiClient\Model\DownloadFile; use CrowdinApiClient\Model\Enterprise\Group; use CrowdinApiClient\Model\Enterprise\GroupManager; use CrowdinApiClient\Model\Enterprise\GroupTeam; @@ -218,6 +219,53 @@ public function testReport(): void $this->assertEquals('50fb3506-4127-4ba8-8296-f97dc7e3e0c3', $report->getIdentifier()); } + public function testGetReport(): void + { + $this->mockRequestGet( + '/groups/123/reports/50fb3506-4127-4ba8-8296-f97dc7e3e0c3', + json_encode([ + 'data' => [ + 'identifier' => '50fb3506-4127-4ba8-8296-f97dc7e3e0c3', + 'status' => 'finished', + 'progress' => 100, + 'attributes' => [ + 'format' => 'xlsx', + 'reportName' => 'costs-estimation', + 'schema' => [], + ], + 'createdAt' => '2019-09-23T11:26:54+00:00', + 'updatedAt' => '2019-09-23T11:26:54+00:00', + 'startedAt' => '2019-09-23T11:26:54+00:00', + 'finishedAt' => '2019-09-23T11:26:54+00:00', + ], + ]) + ); + + $report = $this->crowdin->group->getReport(123, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + + $this->assertInstanceOf(Report::class, $report); + $this->assertEquals('50fb3506-4127-4ba8-8296-f97dc7e3e0c3', $report->getIdentifier()); + $this->assertEquals('finished', $report->getStatus()); + } + + public function testDownloadReport(): void + { + $this->mockRequestGet( + '/groups/123/reports/50fb3506-4127-4ba8-8296-f97dc7e3e0c3/download', + json_encode([ + 'data' => [ + 'url' => 'https://production-enterprise-importer.downloads.crowdin.com/report.xlsx', + 'expireIn' => '2019-09-20T10:31:21+00:00', + ], + ]) + ); + + $file = $this->crowdin->group->downloadReport(123, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + + $this->assertInstanceOf(DownloadFile::class, $file); + $this->assertEquals('https://production-enterprise-importer.downloads.crowdin.com/report.xlsx', $file->getUrl()); + } + public function testListManagers(): void { $this->mockRequest([ diff --git a/tests/CrowdinApiClient/Api/Enterprise/ReportApiTest.php b/tests/CrowdinApiClient/Api/Enterprise/ReportApiTest.php index 5e87d6cc..a98f9983 100644 --- a/tests/CrowdinApiClient/Api/Enterprise/ReportApiTest.php +++ b/tests/CrowdinApiClient/Api/Enterprise/ReportApiTest.php @@ -2,7 +2,10 @@ namespace CrowdinApiClient\Tests\Api\Enterprise; +use CrowdinApiClient\Model\DownloadFile; use CrowdinApiClient\Model\Report; +use CrowdinApiClient\Model\ReportSettingsTemplate; +use CrowdinApiClient\ModelCollection; class ReportApiTest extends AbstractTestApi { @@ -79,4 +82,206 @@ public function testGenerate(): void $this->assertInstanceOf(Report::class, $report); $this->assertEquals('50fb3506-4127-4ba8-8296-f97dc7e3e0c3', $report->getIdentifier()); } + + private const REPORT_ID = '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'; + + private function reportResponse(): string + { + return json_encode([ + 'data' => [ + 'identifier' => self::REPORT_ID, + 'status' => 'finished', + 'progress' => 100, + 'attributes' => [ + 'format' => 'xlsx', + 'reportName' => 'costs-estimation', + 'schema' => [], + ], + 'createdAt' => '2019-09-23T11:26:54+00:00', + 'updatedAt' => '2019-09-23T11:26:54+00:00', + 'startedAt' => '2019-09-23T11:26:54+00:00', + 'finishedAt' => '2019-09-23T11:26:54+00:00', + ], + ]); + } + + private function downloadResponse(): string + { + return json_encode([ + 'data' => [ + 'url' => 'https://production-enterprise-importer.downloads.crowdin.com/report.xlsx', + 'expireIn' => '2019-09-20T10:31:21+00:00', + ], + ]); + } + + private function templateData(): array + { + return [ + 'id' => 12, + 'projectId' => 2, + 'groupId' => null, + 'name' => 'Default template', + 'currency' => 'USD', + 'unit' => 'words', + 'config' => [ + 'baseRates' => ['fullTranslation' => 0.1, 'proofread' => 0.12], + 'individualRates' => [], + 'netRateSchemes' => ['tmMatch' => [], 'mtMatch' => [], 'aiMatch' => [], 'suggestionMatch' => []], + 'calculateInternalMatches' => false, + 'includePreTranslatedStrings' => false, + 'excludeApprovalsForEditedTranslations' => false, + 'preTranslatedStringsCategorizationAdjustment' => false, + ], + 'isPublic' => false, + 'createdAt' => '2023-09-23T11:26:54+00:00', + 'updatedAt' => '2023-09-23T11:26:54+00:00', + ]; + } + + public function testGet(): void + { + $this->mockRequestGet('/reports/' . self::REPORT_ID, $this->reportResponse()); + + $report = $this->crowdin->report->get(self::REPORT_ID); + + $this->assertInstanceOf(Report::class, $report); + $this->assertEquals(self::REPORT_ID, $report->getIdentifier()); + } + + public function testDownload(): void + { + $this->mockRequestGet('/reports/' . self::REPORT_ID . '/download', $this->downloadResponse()); + + $file = $this->crowdin->report->download(self::REPORT_ID); + + $this->assertInstanceOf(DownloadFile::class, $file); + $this->assertEquals('https://production-enterprise-importer.downloads.crowdin.com/report.xlsx', $file->getUrl()); + } + + public function testGenerateProjectReport(): void + { + $data = ['name' => 'costs-estimation', 'schema' => ['unit' => 'words', 'format' => 'xlsx']]; + + $this->mockRequest([ + 'path' => '/projects/2/reports', + 'method' => 'post', + 'body' => json_encode($data), + 'response' => $this->reportResponse(), + ]); + + $report = $this->crowdin->report->generateProjectReport(2, $data); + + $this->assertInstanceOf(Report::class, $report); + $this->assertEquals(self::REPORT_ID, $report->getIdentifier()); + } + + public function testGetProjectReport(): void + { + $this->mockRequestGet('/projects/2/reports/' . self::REPORT_ID, $this->reportResponse()); + + $report = $this->crowdin->report->getProjectReport(2, self::REPORT_ID); + + $this->assertInstanceOf(Report::class, $report); + $this->assertEquals(self::REPORT_ID, $report->getIdentifier()); + } + + public function testDownloadProjectReport(): void + { + $this->mockRequestGet('/projects/2/reports/' . self::REPORT_ID . '/download', $this->downloadResponse()); + + $file = $this->crowdin->report->downloadProjectReport(2, self::REPORT_ID); + + $this->assertInstanceOf(DownloadFile::class, $file); + $this->assertEquals('https://production-enterprise-importer.downloads.crowdin.com/report.xlsx', $file->getUrl()); + } + + public function testListReportSettingsTemplates(): void + { + $this->mockRequestGet( + '/reports/settings-templates?projectId=2', + json_encode([ + 'data' => [['data' => $this->templateData()]], + 'pagination' => ['offset' => 0, 'limit' => 25], + ]) + ); + + $templates = $this->crowdin->report->listReportSettingsTemplates(['projectId' => 2]); + + $this->assertInstanceOf(ModelCollection::class, $templates); + $this->assertCount(1, $templates); + $this->assertInstanceOf(ReportSettingsTemplate::class, $templates[0]); + $this->assertEquals(12, $templates[0]->getId()); + } + + public function testGetReportSettingsTemplate(): void + { + $this->mockRequestGet('/reports/settings-templates/12', json_encode(['data' => $this->templateData()])); + + $template = $this->crowdin->report->getReportSettingsTemplate(12); + + $this->assertInstanceOf(ReportSettingsTemplate::class, $template); + $this->assertEquals(12, $template->getId()); + $this->assertEquals(2, $template->getProjectId()); + $this->assertNull($template->getGroupId()); + } + + public function testCreateReportSettingsTemplate(): void + { + $data = $this->templateData(); + unset($data['id'], $data['groupId'], $data['createdAt'], $data['updatedAt']); + + $this->mockRequest([ + 'path' => '/reports/settings-templates', + 'method' => 'post', + 'body' => json_encode($data), + 'response' => json_encode(['data' => $this->templateData()]), + ]); + + $template = $this->crowdin->report->createReportSettingsTemplate($data); + + $this->assertInstanceOf(ReportSettingsTemplate::class, $template); + $this->assertEquals(12, $template->getId()); + } + + public function testCreateReportSettingsTemplateFromModelSkipsReadOnlyKeysAndNulls(): void + { + $template = new ReportSettingsTemplate($this->templateData()); + $expected = $template->toArray(); + unset($expected['id'], $expected['groupId'], $expected['isGlobal'], $expected['createdAt'], $expected['updatedAt']); + + $this->mockRequest([ + 'path' => '/reports/settings-templates', + 'method' => 'post', + 'body' => json_encode($expected), + 'response' => json_encode(['data' => $this->templateData()]), + ]); + + $this->crowdin->report->createReportSettingsTemplate($template->toArray()); + } + + public function testUpdateReportSettingsTemplate(): void + { + $template = new ReportSettingsTemplate($this->templateData()); + $template->setName('New name'); + + $this->mockRequest([ + 'path' => '/reports/settings-templates/12', + 'method' => 'patch', + 'body' => json_encode([['op' => 'replace', 'path' => '/name', 'value' => 'New name']]), + 'response' => json_encode(['data' => array_merge($this->templateData(), ['name' => 'New name'])]), + ]); + + $updated = $this->crowdin->report->updateReportSettingsTemplate($template); + + $this->assertInstanceOf(ReportSettingsTemplate::class, $updated); + $this->assertEquals('New name', $updated->getName()); + } + + public function testDeleteReportSettingsTemplate(): void + { + $this->mockRequestDelete('/reports/settings-templates/12'); + + $this->crowdin->report->deleteReportSettingsTemplate(12); + } } diff --git a/tests/CrowdinApiClient/Api/ReportApiTest.php b/tests/CrowdinApiClient/Api/ReportApiTest.php index c6b8da4b..8af00ea3 100644 --- a/tests/CrowdinApiClient/Api/ReportApiTest.php +++ b/tests/CrowdinApiClient/Api/ReportApiTest.php @@ -396,6 +396,34 @@ public function testCreateReportSettingsTemplate(): void $this->assertInstanceOf(ReportSettingsTemplateConfig::class, $reportSettingsTemplate->getConfig()); } + public function testCreateReportSettingsTemplateFromModelSkipsReadOnlyKeys(): void + { + $template = new ReportSettingsTemplate([ + 'id' => 12, + 'projectId' => 87, + 'groupId' => null, + 'name' => 'Default template', + 'currency' => 'UAH', + 'unit' => 'words', + 'config' => [], + 'isPublic' => true, + 'isGlobal' => false, + 'createdAt' => '2025-01-23T15:23:11+00:00', + 'updatedAt' => null, + ]); + $expected = $template->toArray(); + unset($expected['id'], $expected['projectId'], $expected['groupId'], $expected['createdAt'], $expected['updatedAt']); + + $this->mockRequest([ + 'path' => '/projects/87/reports/settings-templates', + 'method' => 'post', + 'body' => json_encode($expected), + 'response' => json_encode(['data' => $template->toArray()]), + ]); + + $this->crowdin->report->createReportSettingsTemplate(87, $template->toArray()); + } + public function testUpdateReportSettingsTemplate(): void { $this->mockRequest([ diff --git a/tests/CrowdinApiClient/Model/ReportSettingsTemplateTest.php b/tests/CrowdinApiClient/Model/ReportSettingsTemplateTest.php index c17f800d..6d5dc802 100644 --- a/tests/CrowdinApiClient/Model/ReportSettingsTemplateTest.php +++ b/tests/CrowdinApiClient/Model/ReportSettingsTemplateTest.php @@ -13,6 +13,8 @@ class ReportSettingsTemplateTest extends TestCase { public $data = [ 'id' => 12, + 'projectId' => null, + 'groupId' => null, 'name' => 'Default template', 'currency' => 'UAH', 'unit' => 'words', @@ -87,6 +89,28 @@ public function testLoadData(): void $this->assertEquals($this->data['isGlobal'], $this->reportSettingsTemplate->getIsGlobal()); $this->assertEquals($this->data['createdAt'], $this->reportSettingsTemplate->getCreatedAt()); $this->assertEquals($this->data['updatedAt'], $this->reportSettingsTemplate->getUpdatedAt()); + $this->assertNull($this->reportSettingsTemplate->getProjectId()); + $this->assertNull($this->reportSettingsTemplate->getGroupId()); + } + + public function testLoadDataWithProjectAndGroup(): void + { + $reportSettingsTemplate = new ReportSettingsTemplate( + array_merge($this->data, ['projectId' => 12, 'groupId' => 3]) + ); + + $this->assertSame(12, $reportSettingsTemplate->getProjectId()); + $this->assertSame(3, $reportSettingsTemplate->getGroupId()); + } + + public function testLoadDataCastsNumericStringIds(): void + { + $reportSettingsTemplate = new ReportSettingsTemplate( + array_merge($this->data, ['projectId' => '12', 'groupId' => '3']) + ); + + $this->assertSame(12, $reportSettingsTemplate->getProjectId()); + $this->assertSame(3, $reportSettingsTemplate->getGroupId()); } /** From 626ccfdb758cb907a21384ba7aec85ab0e9c04eb Mon Sep 17 00:00:00 2001 From: Bohdan Bobko <172895427+bhdnb@users.noreply.github.com> Date: Wed, 30 Sep 2026 16:39:04 +0300 Subject: [PATCH 2/9] chore: mark deprecated API fields and fix @link operation ids --- src/CrowdinApiClient/Api/AiApi.php | 18 ++++++------ src/CrowdinApiClient/Api/BundleApi.php | 4 +-- src/CrowdinApiClient/Api/DistributionApi.php | 8 +++--- src/CrowdinApiClient/Api/Enterprise/AiApi.php | 10 +++---- .../Api/Enterprise/TeamApi.php | 10 +++++-- .../Api/Enterprise/UserApi.php | 2 +- .../Api/Enterprise/WorkflowStepApi.php | 2 +- src/CrowdinApiClient/Api/GlossaryApi.php | 9 +++--- src/CrowdinApiClient/Api/ReportApi.php | 5 ---- src/CrowdinApiClient/Api/StringCommentApi.php | 2 +- .../Api/StringTranslationApi.php | 4 +-- src/CrowdinApiClient/Api/TaskApi.php | 5 ++-- src/CrowdinApiClient/Api/TranslationApi.php | 17 +++++------ .../Api/TranslationMemoryApi.php | 4 +-- .../Api/TranslationStatusApi.php | 6 ++-- src/CrowdinApiClient/Model/Project.php | 28 +++++++++++++++++++ .../Model/ReportSettingsTemplateConfig.php | 12 ++++++++ src/CrowdinApiClient/Model/Screenshot.php | 15 ++++++++++ src/CrowdinApiClient/Model/SourceString.php | 3 ++ src/CrowdinApiClient/Model/Task.php | 1 + src/CrowdinApiClient/Model/TaskForUpdate.php | 2 ++ tests/CrowdinApiClient/Api/ProjectApiTest.php | 17 +++++++++++ tests/CrowdinApiClient/Model/ProjectTest.php | 12 ++++++++ .../CrowdinApiClient/Model/ScreenshotTest.php | 10 +++++++ 24 files changed, 153 insertions(+), 53 deletions(-) diff --git a/src/CrowdinApiClient/Api/AiApi.php b/src/CrowdinApiClient/Api/AiApi.php index 89c7a61e..5c3ba373 100644 --- a/src/CrowdinApiClient/Api/AiApi.php +++ b/src/CrowdinApiClient/Api/AiApi.php @@ -131,7 +131,7 @@ public function downloadFileTranslationStrings(int $userId, string $jobIdentifie /** * List AI Prompts - * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.prompts.getMany API Documentation + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.prompts.getMany API Documentation * * @param int $userId * @param array $params @@ -228,7 +228,7 @@ public function clonePrompt(int $userId, int $aiPromptId, array $data): ?AiPromp /** * Create AI Prompt Completion - * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.prompts.completions.post API Documentation + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.prompts.completions.post API Documentation * * @param int $userId * @param int $aiPromptId @@ -291,7 +291,7 @@ public function downloadPromptCompletion(int $userId, int $aiPromptId, string $c /** * List AI Providers - * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.providers.getMany API Documentation + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.providers.getMany API Documentation * * @param int $userId * @param array $params @@ -369,7 +369,7 @@ public function deleteProvider(int $userId, int $aiProviderId) /** * List AI Provider Models - * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.providers.models.getMany API Documentation + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.providers.models.getMany API Documentation * * @param int $userId * @param int $aiProviderId @@ -508,7 +508,7 @@ public function updateSettings(int $userId, AiSettings $aiSettings): ?AiSettings /** * List AI Snippets - * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.settings.snippets.getMany API Documentation + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.prompts.snippets.getMany API Documentation * * @param int $userId * @param array $params @@ -524,7 +524,7 @@ public function listSnippets(int $userId, array $params = []): ModelCollection /** * Add AI Snippet - * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.settings.snippets.post API Documentation + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.prompts.snippets.post API Documentation * * @param int $userId * @param array $data @@ -541,7 +541,7 @@ public function createSnippet(int $userId, array $data): ?AiSnippet /** * Get AI Snippet - * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.settings.snippets.get API Documentation + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.prompts.snippets.get API Documentation * * @param int $userId * @param int $aiSnippetId @@ -555,7 +555,7 @@ public function getSnippet(int $userId, int $aiSnippetId): ?AiSnippet /** * Edit AI Snippet - * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.settings.snippets.patch API Documentation + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.prompts.snippets.patch API Documentation * * @param int $userId * @param AiSnippet $aiSnippet @@ -569,7 +569,7 @@ public function updateSnippet(int $userId, AiSnippet $aiSnippet): ?AiSnippet /** * Delete AI Snippet - * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.settings.snippets.delete API Documentation + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.prompts.snippets.delete API Documentation * * @param int $userId * @param int $aiSnippetId diff --git a/src/CrowdinApiClient/Api/BundleApi.php b/src/CrowdinApiClient/Api/BundleApi.php index 14e7b018..a777dcaa 100644 --- a/src/CrowdinApiClient/Api/BundleApi.php +++ b/src/CrowdinApiClient/Api/BundleApi.php @@ -86,8 +86,8 @@ public function delete(int $projectId, int $bundleId) /** * Edit Bundle - * @link https://developer.crowdin.com/api/v2/#operation/api.projects.branches.patch API Documentation - * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.branches.patch API Documentation Enterprise + * @link https://developer.crowdin.com/api/v2/#operation/api.projects.bundles.patch API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.bundles.patch API Documentation Enterprise * * @param int $projectId * @param Bundle $bundle diff --git a/src/CrowdinApiClient/Api/DistributionApi.php b/src/CrowdinApiClient/Api/DistributionApi.php index c0520475..257f05c9 100644 --- a/src/CrowdinApiClient/Api/DistributionApi.php +++ b/src/CrowdinApiClient/Api/DistributionApi.php @@ -51,9 +51,9 @@ public function get(int $projectId, string $hash): ?Distribution * * @param int $projectId * @param array $data - * string $data[exportMode] Enum: "default" "bundle" Default: "default" Note: String-based projects use "bundle" only
+ * string $data[exportMode] Deprecated. Enum: "default" "bundle" Default: "default" Note: String-based projects use "bundle" only
* string $data[name] required
- * int[] $data[fileIds] Required for file-based projects (default export mode)
+ * int[] $data[fileIds] Deprecated, use bundleIds instead. Required for file-based projects (default export mode)
* int[] $data[bundleIds] Required for string-based projects
* string $data[format] required for 'bundle' export mode
* string $data[exportPattern] required for 'bundle' export mode. Note: Can't contain \\ / : * ? \" < > | symbols
@@ -68,7 +68,7 @@ public function create(int $projectId, array $data): ?Distribution /** * Edit Distribution - * @link https://developer.crowdin.com/api/v2/#operation/api.projects.distributions.post API Documentation + * @link https://developer.crowdin.com/api/v2/#operation/api.projects.distributions.patch API Documentation * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.distributions.patch API Documentation Enterprise */ public function update(int $projectId, Distribution $distribution): ?Distribution @@ -102,7 +102,7 @@ public function release(int $projectId, string $hash): ?DistributionRelease /** * Get Distribution Release * @link https://developer.crowdin.com/api/v2/#operation/api.projects.distributions.release.get API Documentation - * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.distributions.release.post API Documentation Enterprise + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.distributions.release.get API Documentation Enterprise */ public function getRelease(int $projectId, string $hash): ?DistributionRelease { diff --git a/src/CrowdinApiClient/Api/Enterprise/AiApi.php b/src/CrowdinApiClient/Api/Enterprise/AiApi.php index 0913ae43..922a9519 100644 --- a/src/CrowdinApiClient/Api/Enterprise/AiApi.php +++ b/src/CrowdinApiClient/Api/Enterprise/AiApi.php @@ -467,7 +467,7 @@ public function updateSettings(AiSettings $aiSettings): ?AiSettings /** * List AI Snippets - * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.settings.snippets.getMany API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.snippets.getMany API Documentation * * @param array $params * integer $params[limit]
@@ -481,7 +481,7 @@ public function listSnippets(array $params = []): ModelCollection /** * Add AI Snippet - * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.settings.snippets.post API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.snippets.post API Documentation * * @param array $data * string $data[description] required
@@ -496,7 +496,7 @@ public function createSnippet(array $data): ?AiSnippet /** * Get AI Snippet - * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.settings.snippets.get API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.snippets.get API Documentation * * @param int $aiSnippetId * @return AiSnippet|null @@ -509,7 +509,7 @@ public function getSnippet(int $aiSnippetId): ?AiSnippet /** * Edit AI Snippet - * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.settings.snippets.patch API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.snippets.patch API Documentation * * @param AiSnippet $aiSnippet * @return AiSnippet|null @@ -522,7 +522,7 @@ public function updateSnippet(AiSnippet $aiSnippet): ?AiSnippet /** * Delete AI Snippet - * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.settings.snippets.delete API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.snippets.delete API Documentation * * @param int $aiSnippetId * @return mixed diff --git a/src/CrowdinApiClient/Api/Enterprise/TeamApi.php b/src/CrowdinApiClient/Api/Enterprise/TeamApi.php index c584e1bd..b045816a 100644 --- a/src/CrowdinApiClient/Api/Enterprise/TeamApi.php +++ b/src/CrowdinApiClient/Api/Enterprise/TeamApi.php @@ -21,9 +21,10 @@ class TeamApi extends AbstractApi * @param int $projectId * @param array $data * integer $data[teamId] required
- * boolean $data[accessToAllWorkflowSteps]
+ * boolean $data[accessToAllWorkflowSteps] Deprecated, use roles instead
* boolean $data[managerAccess]
- * array $data[permissions] + * array $data[permissions] Deprecated, use roles instead
+ * array $data[roles] * @return AddedProjectTeamInfo */ public function addTeamToProject(int $projectId, array $data): AddedProjectTeamInfo @@ -59,7 +60,7 @@ public function create(array $data): Team /** * Get Team - * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.teams.post + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.teams.get * * @param int $teamId * @return Team @@ -82,6 +83,9 @@ public function delete(int $teamId) } /** + * Edit Team + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.teams.patch API Documentation + * * @param Team $team * @return Team */ diff --git a/src/CrowdinApiClient/Api/Enterprise/UserApi.php b/src/CrowdinApiClient/Api/Enterprise/UserApi.php index f64b3a44..cc49e2cf 100644 --- a/src/CrowdinApiClient/Api/Enterprise/UserApi.php +++ b/src/CrowdinApiClient/Api/Enterprise/UserApi.php @@ -44,7 +44,7 @@ public function listProjectMembers(int $projectId, array $data = []): ModelColle /** * Get Project Member Permissions - * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.members.post API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.members.get API Documentation * * @param int $projectId * @param int $memberId diff --git a/src/CrowdinApiClient/Api/Enterprise/WorkflowStepApi.php b/src/CrowdinApiClient/Api/Enterprise/WorkflowStepApi.php index 9523de2d..e7dc1ec2 100644 --- a/src/CrowdinApiClient/Api/Enterprise/WorkflowStepApi.php +++ b/src/CrowdinApiClient/Api/Enterprise/WorkflowStepApi.php @@ -43,7 +43,7 @@ public function get(int $projectId, int $stepId): ?WorkflowStep /** * List Strings on the Workflow Step - * @link https://support.crowdin.com/developer/enterprise/api/v2/#tag/Workflows/operation/api.projects.workflow-steps.get API Documentation + * @link https://support.crowdin.com/developer/enterprise/api/v2/#tag/Workflows/operation/api.projects.workflow-steps.strings.getMany API Documentation */ public function listStrings(int $projectId, int $stepId, array $params = []): ModelCollection { diff --git a/src/CrowdinApiClient/Api/GlossaryApi.php b/src/CrowdinApiClient/Api/GlossaryApi.php index d273a13c..0cd57849 100644 --- a/src/CrowdinApiClient/Api/GlossaryApi.php +++ b/src/CrowdinApiClient/Api/GlossaryApi.php @@ -194,7 +194,7 @@ public function getImport(int $glossaryId, string $importId): ?GlossaryImport * string $data[sourceLanguageId]
* string $data[targetLanguageId]
* string[] $data[expressions]
- * string $data[expression]
+ * string $data[expression] Deprecated, use expressions instead
* @return ModelCollection|null */ public function concordance(int $projectId, array $data): ?ModelCollection @@ -266,7 +266,8 @@ public function listTerms(int $glossaryId, array $params = []): ?ModelCollection * string $data[text] required
* string $data[description]
* string $data[partOfSpeech]
- * integer $data[translationOfTermId] + * integer $data[translationOfTermId] Deprecated, use conceptId instead
+ * integer $data[conceptId] * @return mixed */ public function createTerm(int $glossaryId, array $data): ?Term @@ -335,8 +336,8 @@ public function updateTerm(Term $term): ?Term /** * List Concepts - * @link https://developer.crowdin.com/api/v2/#operation/api.glossaries.terms.getMany API Documentation - * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.glossaries.terms.getMany API Documentation Enterprise + * @link https://developer.crowdin.com/api/v2/#operation/api.glossaries.concepts.getMany API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.glossaries.concepts.getMany API Documentation Enterprise */ public function listConcepts(int $glossaryId, array $params = []): ?ModelCollection { diff --git a/src/CrowdinApiClient/Api/ReportApi.php b/src/CrowdinApiClient/Api/ReportApi.php index b1a5c4b2..309c54a8 100644 --- a/src/CrowdinApiClient/Api/ReportApi.php +++ b/src/CrowdinApiClient/Api/ReportApi.php @@ -59,7 +59,6 @@ public function download(int $projectId, string $reportId): ?DownloadFile /** * List Report Settings Templates * @link https://developer.crowdin.com/api/v2/#operation/api.projects.reports.settings-templates.getMany API Documentation - * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.reports.settings-templates.getMany API Documentation Enterprise */ public function listReportSettingsTemplates(int $projectId): ModelCollection { @@ -70,7 +69,6 @@ public function listReportSettingsTemplates(int $projectId): ModelCollection /** * Get Report Settings Template * @link https://developer.crowdin.com/api/v2/#operation/api.projects.reports.settings-templates.get API Documentation - * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.reports.settings-templates.get API Documentation Enterprise */ public function getReportSettingsTemplate(int $projectId, int $reportSettingsTemplateId): ?ReportSettingsTemplate { @@ -81,7 +79,6 @@ public function getReportSettingsTemplate(int $projectId, int $reportSettingsTem /** * Add Report Settings Template * @link https://developer.crowdin.com/api/v2/#operation/api.projects.reports.settings-templates.post API Documentation - * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.reports.settings-templates.post API Documentation Enterprise */ public function createReportSettingsTemplate(int $projectId, array $data): ?ReportSettingsTemplate { @@ -100,7 +97,6 @@ public function createReportSettingsTemplate(int $projectId, array $data): ?Repo /** * Edit Report Settings Template * @link https://developer.crowdin.com/api/v2/#operation/api.projects.reports.settings-templates.patch API Documentation - * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.reports.settings-templates.patch API Documentation Enterprise */ public function updateReportSettingsTemplate( int $projectId, @@ -113,7 +109,6 @@ public function updateReportSettingsTemplate( /** * Delete Report Settings Template * @link https://developer.crowdin.com/api/v2/#operation/api.projects.reports.settings-templates.delete API Documentation - * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.reports.settings-templates.delete API Documentation Enterprise */ public function deleteReportSettingsTemplate(int $projectId, int $reportSettingsTemplateId): void { diff --git a/src/CrowdinApiClient/Api/StringCommentApi.php b/src/CrowdinApiClient/Api/StringCommentApi.php index bc5fa677..682d4777 100644 --- a/src/CrowdinApiClient/Api/StringCommentApi.php +++ b/src/CrowdinApiClient/Api/StringCommentApi.php @@ -15,7 +15,7 @@ class StringCommentApi extends AbstractApi { /** * List String Comment - * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.comments.get API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.comments.getMany API Documentation * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.comments.getMany API Documentation Enterprise * * @param int $projectId diff --git a/src/CrowdinApiClient/Api/StringTranslationApi.php b/src/CrowdinApiClient/Api/StringTranslationApi.php index 90c2fede..46eb9b67 100644 --- a/src/CrowdinApiClient/Api/StringTranslationApi.php +++ b/src/CrowdinApiClient/Api/StringTranslationApi.php @@ -164,8 +164,8 @@ public function search(array $params = []): ModelCollection /** * Add Translation - * @link https://developer.crowdin.com/api/v2/#operation/api.projects.translations.add API Documentation - * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.translations.add API Documentation Enterprise + * @link https://developer.crowdin.com/api/v2/#operation/api.projects.translations.post API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.translations.post API Documentation Enterprise * * @param int $projectId * @param array $data diff --git a/src/CrowdinApiClient/Api/TaskApi.php b/src/CrowdinApiClient/Api/TaskApi.php index bcf1f494..33b1fa1b 100644 --- a/src/CrowdinApiClient/Api/TaskApi.php +++ b/src/CrowdinApiClient/Api/TaskApi.php @@ -100,8 +100,8 @@ public function delete(int $projectId, int $taskId) /** * Export Project Task Strings - * @link https://developer.crowdin.com/api/v2/#operation/api.projects.tasks.export.get API Documentation - * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.tasks.export.get API Documentation Enterprise + * @link https://developer.crowdin.com/api/v2/#operation/api.projects.tasks.exports.post API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.tasks.exports.post API Documentation Enterprise * * @param int $projectId * @param int $taskId @@ -116,7 +116,6 @@ public function exportStrings(int $projectId, int $taskId): ?DownloadFile /** * List Tasks * @link https://developer.crowdin.com/api/v2/#operation/api.users.tasks.getMany API Documentation - * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.users.tasks.getMany API Documentation Enterprise * * @param int $userId * @param array $params diff --git a/src/CrowdinApiClient/Api/TranslationApi.php b/src/CrowdinApiClient/Api/TranslationApi.php index 613d1e28..5bc72727 100644 --- a/src/CrowdinApiClient/Api/TranslationApi.php +++ b/src/CrowdinApiClient/Api/TranslationApi.php @@ -49,7 +49,8 @@ public function listPreTranslations(int $projectId, array $params = []): ModelCo * int $params[engineId]
* string $params[autoApproveOption]
* boolean $params[duplicateTranslations] Works only with TM pre-translation method
- * boolean $params[translateUntranslatedOnly] Works only with TM pre-translation method
+ * boolean $params[translateUntranslatedOnly] Deprecated, use scope instead. Works only with TM pre-translation method
+ * string $params[scope] Enum: "untranslated" "translated" "all" Default: "untranslated" Note: Can't be used with translateUntranslatedOnly
* boolean $params[translateWithPerfectMatchOnly] Works only with TM pre-translation method
* @return PreTranslation|null */ @@ -83,8 +84,8 @@ public function updatePreTranslation(int $projectId, PreTranslation $preTranslat /** * Pre-Translation Report - * @link https://developer.crowdin.com/api/v2/#operation/api.projects.pre-translations.patch API Documentation - * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.pre-translations.patch API Documentation Enterprise + * @link https://developer.crowdin.com/api/v2/#operation/api.projects.pre-translations.report.getReport API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.pre-translations.report.getReport API Documentation Enterprise */ public function getPreTranslationReport(int $projectId, string $preTranslationId): ?PreTranslationReport { @@ -153,7 +154,7 @@ public function buildProjectDirectoryTranslation( * @param string|null $ifNoneMatch * @return DownloadFile|null * string $params[targetLanguageId]
- * boolean $params[exportAsXliff]
+ * boolean $params[exportAsXliff] Deprecated, use TranslationApi::exportProjectTranslation() instead
* boolean $params[skipUntranslatedStrings] true value can't be used with skipUntranslatedFiles=true in same request
* boolean $params[skipUntranslatedFiles] true value can't be used with skipUntranslatedStrings=true in same request
* boolean $params[exportApprovedOnly] @@ -195,8 +196,8 @@ public function getProjectBuilds(int $projectId, array $params = []): ModelColle /** * Build Project Translation - * @link https://developer.crowdin.com/api/v2/#operation/api.projects.translations.builds.build API Documentation - * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.translations.builds.build API Documentation Enterprise + * @link https://developer.crowdin.com/api/v2/#operation/api.projects.translations.builds.post API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.translations.builds.post API Documentation Enterprise * * @param int $projectId * @param array $params @@ -217,7 +218,7 @@ public function buildProject(int $projectId, array $params = []): ?TranslationPr /** * Import Translations * @link https://developer.crowdin.com/api/v2/#operation/api.projects.translations.imports API Documentation - * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.translations.imports API Documentation Enterprise + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.translations.enterprise.imports API Documentation Enterprise * * @param int $projectId * @param array $params @@ -240,7 +241,7 @@ public function importTranslations(int $projectId, array $params): ?TranslationI /** * Check Translation Import Status * @link https://developer.crowdin.com/api/v2/#operation/api.projects.translations.imports.get API Documentation - * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.translations.imports.get API Documentation Enterprise + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.translations.enterprise.imports.get API Documentation Enterprise */ public function getTranslationImportStatus(int $projectId, string $importTranslationId): ?TranslationImport { diff --git a/src/CrowdinApiClient/Api/TranslationMemoryApi.php b/src/CrowdinApiClient/Api/TranslationMemoryApi.php index 82d9419c..4ca29f47 100644 --- a/src/CrowdinApiClient/Api/TranslationMemoryApi.php +++ b/src/CrowdinApiClient/Api/TranslationMemoryApi.php @@ -221,8 +221,8 @@ public function checkExportStatus(int $translationMemoryId, string $exportId): ? /** * Download TM - * @link https://developer.crowdin.com/api/v2/#operation/api.tms.exports.getMany API Documentation - * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.tms.exports.getMany API Documentation Enterprise + * @link https://developer.crowdin.com/api/v2/#operation/api.tms.exports.download.download API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.tms.exports.download.download API Documentation Enterprise */ public function download(int $translationMemoryId, string $exportId): ?DownloadFile { diff --git a/src/CrowdinApiClient/Api/TranslationStatusApi.php b/src/CrowdinApiClient/Api/TranslationStatusApi.php index 0ec6610a..10139f4b 100644 --- a/src/CrowdinApiClient/Api/TranslationStatusApi.php +++ b/src/CrowdinApiClient/Api/TranslationStatusApi.php @@ -72,7 +72,7 @@ public function getFileProgress(int $projectId, int $fileId, array $params = []) /** * Get Language Progress * @link https://developer.crowdin.com/api/v2/#operation/api.projects.languages.files.progress.getMany API Documentation - * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.files.languages.progress.getMany API Documentation Enterprise + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.languages.files.progress.getMany API Documentation Enterprise * * @param int $projectId * @param string $languageId @@ -107,8 +107,8 @@ public function getProjectProgress(int $projectId, array $params = []): ?ModelCo /** * List QA Check Issues - * @link https://developer.crowdin.com/api/v2/#operation/api.projects.qa-check.getMany API Documentation - * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.qa-check.getMany API Documentation Enterprise + * @link https://developer.crowdin.com/api/v2/#operation/api.projects.qa-checks.getMany API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.qa-checks.getMany API Documentation Enterprise * * @param int $projectId * @param array $params diff --git a/src/CrowdinApiClient/Model/Project.php b/src/CrowdinApiClient/Model/Project.php index 1b4e4c61..d8fa353d 100644 --- a/src/CrowdinApiClient/Model/Project.php +++ b/src/CrowdinApiClient/Model/Project.php @@ -227,6 +227,11 @@ class Project extends BaseModel */ protected $glossaryAccess; + /** + * @var string|null + */ + protected $glossaryAccessOption; + /** * @var bool */ @@ -304,6 +309,9 @@ public function __construct(array $data = []) $this->customQaCheckIds = (array)$this->getDataProperty('customQaCheckIds'); $this->languageMapping = (array)$this->getDataProperty('languageMapping'); $this->glossaryAccess = (bool)$this->getDataProperty('glossaryAccess'); + $this->glossaryAccessOption = $this->getDataProperty('glossaryAccessOption') !== null + ? (string)$this->getDataProperty('glossaryAccessOption') + : null; $this->isSuspended = (bool)$this->getDataProperty('isSuspended'); $this->normalizePlaceholder = (bool)$this->getDataProperty('normalizePlaceholder'); $this->saveMetaInfoInSource = (bool)$this->getDataProperty('saveMetaInfoInSource'); @@ -516,6 +524,7 @@ public function setLogo(string $logo): void } /** + * @deprecated Deprecated by the Crowdin Enterprise API. * @return string */ public function getBackground(): string @@ -524,6 +533,7 @@ public function getBackground(): string } /** + * @deprecated Deprecated by the Crowdin Enterprise API. * @param string $background */ public function setBackground(string $background): void @@ -974,6 +984,7 @@ public function setLanguageMapping(array $languageMapping): void } /** + * @deprecated Deprecated by the Crowdin API. Use getGlossaryAccessOption() instead. * @return bool */ public function isGlossaryAccess(): bool @@ -982,6 +993,7 @@ public function isGlossaryAccess(): bool } /** + * @deprecated Deprecated by the Crowdin API. Use setGlossaryAccessOption() instead. * @param bool $glossaryAccess */ public function setGlossaryAccess(bool $glossaryAccess): void @@ -989,6 +1001,22 @@ public function setGlossaryAccess(bool $glossaryAccess): void $this->glossaryAccess = $glossaryAccess; } + /** + * @return string|null readOnly, fullAccess or manageDrafts + */ + public function getGlossaryAccessOption(): ?string + { + return $this->glossaryAccessOption; + } + + /** + * @param string $glossaryAccessOption readOnly, fullAccess or manageDrafts + */ + public function setGlossaryAccessOption(string $glossaryAccessOption): void + { + $this->glossaryAccessOption = $glossaryAccessOption; + } + /** * @return bool */ diff --git a/src/CrowdinApiClient/Model/ReportSettingsTemplateConfig.php b/src/CrowdinApiClient/Model/ReportSettingsTemplateConfig.php index 1b4580ba..0851a648 100644 --- a/src/CrowdinApiClient/Model/ReportSettingsTemplateConfig.php +++ b/src/CrowdinApiClient/Model/ReportSettingsTemplateConfig.php @@ -133,21 +133,33 @@ public function setIncludePreTranslatedStrings(bool $includePreTranslatedStrings $this->includePreTranslatedStrings = $includePreTranslatedStrings; } + /** + * @deprecated Deprecated by the Crowdin API. + */ public function getExcludeApprovalsForEditedTranslations(): bool { return $this->excludeApprovalsForEditedTranslations; } + /** + * @deprecated Deprecated by the Crowdin API. + */ public function setExcludeApprovalsForEditedTranslations(bool $excludeApprovalsForEditedTranslations): void { $this->excludeApprovalsForEditedTranslations = $excludeApprovalsForEditedTranslations; } + /** + * @deprecated Deprecated by the Crowdin API. + */ public function getPreTranslatedStringsCategorizationAdjustment(): bool { return $this->preTranslatedStringsCategorizationAdjustment; } + /** + * @deprecated Deprecated by the Crowdin API. + */ public function setPreTranslatedStringsCategorizationAdjustment( bool $preTranslatedStringsCategorizationAdjustment ): void { diff --git a/src/CrowdinApiClient/Model/Screenshot.php b/src/CrowdinApiClient/Model/Screenshot.php index c11811e8..d80b4111 100644 --- a/src/CrowdinApiClient/Model/Screenshot.php +++ b/src/CrowdinApiClient/Model/Screenshot.php @@ -22,6 +22,11 @@ class Screenshot extends BaseModel */ protected $url; + /** + * @var string|null + */ + protected $webUrl; + /** * @var string */ @@ -63,6 +68,7 @@ public function __construct(array $data = []) $this->id = (int)$this->getDataProperty('id'); $this->userId = (int)$this->getDataProperty('userId'); $this->url = (string)$this->getDataProperty('url'); + $this->webUrl = $this->getDataProperty('webUrl') !== null ? (string)$this->getDataProperty('webUrl') : null; $this->name = (string)$this->getDataProperty('name'); $this->size = (array)$this->getDataProperty('size'); $this->tagsCount = (int)$this->getDataProperty('tagsCount'); @@ -89,6 +95,7 @@ public function getUserId(): int } /** + * @deprecated Deprecated by the Crowdin API. Use getWebUrl() instead. * @return string */ public function getUrl(): string @@ -96,6 +103,14 @@ public function getUrl(): string return $this->url; } + /** + * @return string|null + */ + public function getWebUrl(): ?string + { + return $this->webUrl; + } + /** * @return string */ diff --git a/src/CrowdinApiClient/Model/SourceString.php b/src/CrowdinApiClient/Model/SourceString.php index c709f153..8d17a16d 100644 --- a/src/CrowdinApiClient/Model/SourceString.php +++ b/src/CrowdinApiClient/Model/SourceString.php @@ -286,6 +286,7 @@ public function getRevision(): int } /** + * @deprecated Deprecated by the Crowdin API. Use getType() and getText() instead. * @return bool */ public function isHasPlurals(): bool @@ -295,6 +296,7 @@ public function isHasPlurals(): bool /** * Alias of isHasPlurals + * @deprecated Deprecated by the Crowdin API. Use getType() and getText() instead. * @return bool */ public function isPlural(): bool @@ -303,6 +305,7 @@ public function isPlural(): bool } /** + * @deprecated Deprecated by the Crowdin API. Use getType() instead. * @return bool */ public function isIcu(): bool diff --git a/src/CrowdinApiClient/Model/Task.php b/src/CrowdinApiClient/Model/Task.php index ea814664..cc6b4bd2 100644 --- a/src/CrowdinApiClient/Model/Task.php +++ b/src/CrowdinApiClient/Model/Task.php @@ -404,6 +404,7 @@ public function getProgress(): array } /** + * @deprecated Deprecated by the Crowdin API. * @return array|null */ public function getTranslateProgress(): ?array diff --git a/src/CrowdinApiClient/Model/TaskForUpdate.php b/src/CrowdinApiClient/Model/TaskForUpdate.php index 91e2735c..047375ce 100644 --- a/src/CrowdinApiClient/Model/TaskForUpdate.php +++ b/src/CrowdinApiClient/Model/TaskForUpdate.php @@ -113,6 +113,7 @@ private function initMissingProperties(): void } /** + * @deprecated Deprecated by the Crowdin API. Use getSplitContent() instead. * @return bool */ public function getSplitFiles(): bool @@ -121,6 +122,7 @@ public function getSplitFiles(): bool } /** + * @deprecated Deprecated by the Crowdin API. Use setSplitContent() instead. * @param bool $splitFiles */ public function setSplitFiles(bool $splitFiles): void diff --git a/tests/CrowdinApiClient/Api/ProjectApiTest.php b/tests/CrowdinApiClient/Api/ProjectApiTest.php index a6fec406..d453981b 100644 --- a/tests/CrowdinApiClient/Api/ProjectApiTest.php +++ b/tests/CrowdinApiClient/Api/ProjectApiTest.php @@ -365,6 +365,23 @@ public function testInfoGetAndUpdate(): void $this->assertEquals('edit test', $project->getName()); } + public function testUpdateSendsGlossaryAccessOption(): void + { + $project = new Project(['id' => 8, 'glossaryAccessOption' => 'readOnly']); + $project->setGlossaryAccessOption('fullAccess'); + + $this->mockRequest([ + 'path' => '/projects/8', + 'method' => 'patch', + 'body' => json_encode([['op' => 'replace', 'path' => '/glossaryAccessOption', 'value' => 'fullAccess']]), + 'response' => json_encode(['data' => ['id' => 8, 'glossaryAccessOption' => 'fullAccess']]), + ]); + + $project = $this->crowdin->project->update($project); + + $this->assertEquals('fullAccess', $project->getGlossaryAccessOption()); + } + public function testDelete(): void { $this->mockRequestDelete('/projects/1'); diff --git a/tests/CrowdinApiClient/Model/ProjectTest.php b/tests/CrowdinApiClient/Model/ProjectTest.php index 5ed9011d..c10090c6 100644 --- a/tests/CrowdinApiClient/Model/ProjectTest.php +++ b/tests/CrowdinApiClient/Model/ProjectTest.php @@ -116,6 +116,7 @@ class ProjectTest extends TestCase ], 'glossaryAccess' => true, + 'glossaryAccessOption' => 'fullAccess', 'normalizePlaceholder' => false, 'saveMetaInfoInSource' => false, 'notificationSettings' => [ @@ -191,6 +192,7 @@ public function testLoadData(): void $this->assertEquals($this->data['isSuspended'], $project->isSuspended()); $this->assertEquals($this->data['glossaryAccess'], $project->isGlossaryAccess()); + $this->assertEquals($this->data['glossaryAccessOption'], $project->getGlossaryAccessOption()); $this->assertEquals($this->data['normalizePlaceholder'], $project->isNormalizePlaceholder()); $this->assertEquals($this->data['saveMetaInfoInSource'], $project->isSaveMetaInfoInSource()); $this->assertEquals($this->data['notificationSettings'], $project->getNotificationSettings()); @@ -214,6 +216,7 @@ public function testSetData(): void $project->setDefaultTmId($this->data['defaultTmId']); $project->setDefaultGlossaryId($this->data['defaultGlossaryId']); $project->setFields($fields); + $project->setGlossaryAccessOption('manageDrafts'); $this->assertEquals($this->data['name'], $project->getName()); $this->assertEquals($this->data['cname'], $project->getCname()); @@ -223,5 +226,14 @@ public function testSetData(): void $this->assertEquals($this->data['defaultTmId'], $project->getDefaultTmId()); $this->assertEquals($this->data['defaultGlossaryId'], $project->getDefaultGlossaryId()); $this->assertEquals($fields, $project->getFields()); + $this->assertEquals('manageDrafts', $project->getGlossaryAccessOption()); + } + + public function testLoadDataWithoutGlossaryAccessOption(): void + { + $data = $this->data; + unset($data['glossaryAccessOption']); + + $this->assertNull((new Project($data))->getGlossaryAccessOption()); } } diff --git a/tests/CrowdinApiClient/Model/ScreenshotTest.php b/tests/CrowdinApiClient/Model/ScreenshotTest.php index 6421b595..51b66c13 100644 --- a/tests/CrowdinApiClient/Model/ScreenshotTest.php +++ b/tests/CrowdinApiClient/Model/ScreenshotTest.php @@ -19,6 +19,7 @@ class ScreenshotTest extends TestCase 'id' => 2, 'userId' => 6, 'url' => 'https://production-enterprise-screenshots.downloads.crowdin.com/992000002/6/2/middle.jpg?X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIGJKLQV66ZXPMMEA%2F20190923%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20190923T093016Z&X-Amz-SignedHeaders=host&X-Amz-Expires=120&X-Amz-Signature=8df06f57594f7d1804b7c037629f6916224415e9b935c4f6619fbe002fb25e73', + 'webUrl' => 'https://crowdin.com/project/example/screenshots/2', 'name' => 'translate_with_siri.jpg', 'size' => [ @@ -71,11 +72,20 @@ public function testSetData() $this->assertEquals($this->data['name'], $this->screenshot->getName()); } + public function testLoadDataWithoutWebUrl(): void + { + $data = $this->data; + unset($data['webUrl']); + + $this->assertNull((new Screenshot($data))->getWebUrl()); + } + public function checkData() { $this->assertEquals($this->data['id'], $this->screenshot->getId()); $this->assertEquals($this->data['userId'], $this->screenshot->getUserId()); $this->assertEquals($this->data['url'], $this->screenshot->getUrl()); + $this->assertEquals($this->data['webUrl'], $this->screenshot->getWebUrl()); $this->assertEquals($this->data['name'], $this->screenshot->getName()); $this->assertEquals($this->data['size'], $this->screenshot->getSize()); $this->assertEquals($this->data['tagsCount'], $this->screenshot->getTagsCount()); From 2813b01659784d53269266d6e89177c67db2661b Mon Sep 17 00:00:00 2001 From: Bohdan Bobko <172895427+bhdnb@users.noreply.github.com> Date: Wed, 30 Sep 2026 17:00:04 +0300 Subject: [PATCH 3/9] feat: add label-string assignment, TM segment records and QA validation --- src/CrowdinApiClient/Api/LabelApi.php | 60 +- src/CrowdinApiClient/Api/TranslationApi.php | 26 + .../Api/TranslationMemoryApi.php | 72 +++ src/CrowdinApiClient/Model/QaCheck.php | 32 + tests/CrowdinApiClient/Api/LabelApiTest.php | 276 +++++---- .../Api/TranslationApiTest.php | 41 ++ .../Api/TranslationMemoryApiTest.php | 545 ++++++++++-------- tests/CrowdinApiClient/Model/QaCheckTest.php | 13 + 8 files changed, 700 insertions(+), 365 deletions(-) diff --git a/src/CrowdinApiClient/Api/LabelApi.php b/src/CrowdinApiClient/Api/LabelApi.php index a799438d..510d07b5 100644 --- a/src/CrowdinApiClient/Api/LabelApi.php +++ b/src/CrowdinApiClient/Api/LabelApi.php @@ -5,6 +5,7 @@ use CrowdinApiClient\Http\ResponseDecorator\ResponseModelListDecorator; use CrowdinApiClient\Model\Label; use CrowdinApiClient\Model\Screenshot; +use CrowdinApiClient\Model\SourceString; use CrowdinApiClient\ModelCollection; /** @@ -130,11 +131,66 @@ public function unassignScreenshots(int $projectId, int $labelId, array $data): { $path = sprintf('projects/%d/labels/%d/screenshots', $projectId, $labelId); + return $this->client->apiRequest( + 'delete', + $path, + new ResponseModelListDecorator(Screenshot::class), + ['params' => ['screenshotIds' => $this->joinIds($data['screenshotIds'] ?? [])]] + ); + } + + /** + * Assign Label to Strings + * @link https://developer.crowdin.com/api/v2/#operation/api.projects.labels.strings.post API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.labels.strings.post API Documentation Enterprise + * + * @param int $projectId + * @param int $labelId + * @param array $data + * int[] $data[stringIds] required Note: You can assign up to 500 strings at a time + * @return ModelCollection + */ + public function assignStrings(int $projectId, int $labelId, array $data): ModelCollection + { + $path = sprintf('projects/%d/labels/%d/strings', $projectId, $labelId); + $options = [ 'body' => json_encode($data), - 'headers' => ['Content-Type' => 'application/json'] + 'headers' => $this->getHeaders(), ]; - return $this->client->apiRequest('delete', $path, new ResponseModelListDecorator(Screenshot::class), $options); + return $this->client->apiRequest('post', $path, new ResponseModelListDecorator(SourceString::class), $options); + } + + /** + * Unassign Label from Strings + * @link https://developer.crowdin.com/api/v2/#operation/api.projects.labels.strings.deleteMany API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.labels.strings.deleteMany API Documentation Enterprise + * + * @param int $projectId + * @param int $labelId + * @param array $data + * int[] $data[stringIds] required Note: You can unassign up to 500 strings at a time + * @return ModelCollection + */ + public function unassignStrings(int $projectId, int $labelId, array $data): ModelCollection + { + $path = sprintf('projects/%d/labels/%d/strings', $projectId, $labelId); + + return $this->client->apiRequest( + 'delete', + $path, + new ResponseModelListDecorator(SourceString::class), + ['params' => ['stringIds' => $this->joinIds($data['stringIds'] ?? [])]] + ); + } + + /** + * @param int[]|string[]|string $ids + * @return string + */ + private function joinIds($ids): string + { + return is_array($ids) ? implode(',', $ids) : (string)$ids; } } diff --git a/src/CrowdinApiClient/Api/TranslationApi.php b/src/CrowdinApiClient/Api/TranslationApi.php index 5bc72727..c2d07b39 100644 --- a/src/CrowdinApiClient/Api/TranslationApi.php +++ b/src/CrowdinApiClient/Api/TranslationApi.php @@ -8,6 +8,7 @@ use CrowdinApiClient\Model\DownloadFileTranslation; use CrowdinApiClient\Model\PreTranslation; use CrowdinApiClient\Model\PreTranslationReport; +use CrowdinApiClient\Model\QaCheck; use CrowdinApiClient\Model\TranslationAlignment; use CrowdinApiClient\Model\TranslationImport; use CrowdinApiClient\Model\TranslationImportReport; @@ -368,4 +369,29 @@ public function alignment(int $projectId, array $params): ?TranslationAlignment $path = sprintf('projects/%d/translations/alignment', $projectId); return $this->_post($path, TranslationAlignment::class, $params); } + + /** + * Validate QA Checks + * @link https://developer.crowdin.com/api/v2/#operation/api.projects.translations.validate-qa-checks.post API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.translations.validate-qa-checks.post API Documentation Enterprise + * + * @param int $projectId + * @param array $data list of translations to validate
+ * integer $data[][stringId] required
+ * string $data[][languageId] required
+ * string $data[][text] required
+ * string $data[][pluralCategoryName] Enum: "zero" "one" "two" "few" "many" "other" + * @return ModelCollection + */ + public function validateQaChecks(int $projectId, array $data): ModelCollection + { + $path = sprintf('projects/%d/translations/validate-qa-checks', $projectId); + + $options = [ + 'body' => json_encode($data), + 'headers' => $this->getHeaders(), + ]; + + return $this->client->apiRequest('post', $path, new ResponseModelListDecorator(QaCheck::class), $options); + } } diff --git a/src/CrowdinApiClient/Api/TranslationMemoryApi.php b/src/CrowdinApiClient/Api/TranslationMemoryApi.php index 4ca29f47..dc03be47 100644 --- a/src/CrowdinApiClient/Api/TranslationMemoryApi.php +++ b/src/CrowdinApiClient/Api/TranslationMemoryApi.php @@ -190,6 +190,78 @@ public function deleteSegment(int $tmId, int $segmentId) return $this->_delete($path); } + /** + * Edit TM Segment + * @link https://developer.crowdin.com/api/v2/#operation/api.tms.segments.patch API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.tms.segments.patch API Documentation Enterprise + * + * @param int $tmId + * @param int $segmentId + * @param array $data JSON Patch array. Paths: /records/- (add), /records/{recordId}/text (replace), /records/{recordId} (remove) + * @return TranslationMemorySegment|null + */ + public function updateSegment(int $tmId, int $segmentId, array $data): ?TranslationMemorySegment + { + $path = sprintf('tms/%d/segments/%d', $tmId, $segmentId); + return $this->_patch($path, TranslationMemorySegment::class, $data); + } + + /** + * Create TM Segment Records + * @link https://developer.crowdin.com/api/v2/#operation/api.tms.segments.records.post API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.tms.segments.records.post API Documentation Enterprise + * + * @param int $tmId + * @param int $segmentId + * @param array $data + * array $data[records] required
+ * string $data[records][][languageId]
+ * string $data[records][][text] + * @return TranslationMemorySegment|null + */ + public function createSegmentRecords(int $tmId, int $segmentId, array $data): ?TranslationMemorySegment + { + $path = sprintf('tms/%d/segments/%d/records', $tmId, $segmentId); + return $this->_create($path, TranslationMemorySegment::class, $data); + } + + /** + * Edit TM Segment Record + * @link https://developer.crowdin.com/api/v2/#operation/api.tms.segments.records.patch API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.tms.segments.records.patch API Documentation Enterprise + * + * @param int $tmId + * @param int $segmentId + * @param int $recordId + * @param array $data JSON Patch array. Path: /text (replace) + * @return TranslationMemorySegment|null + */ + public function updateSegmentRecord( + int $tmId, + int $segmentId, + int $recordId, + array $data + ): ?TranslationMemorySegment { + $path = sprintf('tms/%d/segments/%d/records/%d', $tmId, $segmentId, $recordId); + return $this->_patch($path, TranslationMemorySegment::class, $data); + } + + /** + * Delete TM Segment Record + * @link https://developer.crowdin.com/api/v2/#operation/api.tms.segments.records.delete API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.tms.segments.records.delete API Documentation Enterprise + * + * @param int $tmId + * @param int $segmentId + * @param int $recordId + * @return mixed + */ + public function deleteSegmentRecord(int $tmId, int $segmentId, int $recordId) + { + $path = sprintf('tms/%d/segments/%d/records/%d', $tmId, $segmentId, $recordId); + return $this->_delete($path); + } + /** * Export TM * @link https://developer.crowdin.com/api/v2/#operation/api.tms.exports.post API Documentation diff --git a/src/CrowdinApiClient/Model/QaCheck.php b/src/CrowdinApiClient/Model/QaCheck.php index f20c71ba..ae62c095 100644 --- a/src/CrowdinApiClient/Model/QaCheck.php +++ b/src/CrowdinApiClient/Model/QaCheck.php @@ -47,6 +47,16 @@ class QaCheck extends BaseModel */ protected $text; + /** + * @var string|null + */ + protected $pluralCategoryName; + + /** + * @var string|null + */ + protected $translation; + public function __construct(array $data = []) { parent::__construct($data); @@ -58,6 +68,12 @@ public function __construct(array $data = []) $this->validationDescription = (string)$this->getDataProperty('validationDescription'); $this->pluralId = (int)$this->getDataProperty('pluralId'); $this->text = (string)$this->getDataProperty('text'); + $this->pluralCategoryName = $this->getDataProperty('pluralCategoryName') !== null + ? (string)$this->getDataProperty('pluralCategoryName') + : null; + $this->translation = $this->getDataProperty('translation') !== null + ? (string)$this->getDataProperty('translation') + : null; } /** @@ -123,4 +139,20 @@ public function getText(): string { return $this->text; } + + /** + * @return string|null + */ + public function getPluralCategoryName(): ?string + { + return $this->pluralCategoryName; + } + + /** + * @return string|null + */ + public function getTranslation(): ?string + { + return $this->translation; + } } diff --git a/tests/CrowdinApiClient/Api/LabelApiTest.php b/tests/CrowdinApiClient/Api/LabelApiTest.php index 64c94dda..f194ee20 100644 --- a/tests/CrowdinApiClient/Api/LabelApiTest.php +++ b/tests/CrowdinApiClient/Api/LabelApiTest.php @@ -4,6 +4,7 @@ use CrowdinApiClient\Model\Label; use CrowdinApiClient\Model\Screenshot; +use CrowdinApiClient\Model\SourceString; use CrowdinApiClient\ModelCollection; class LabelApiTest extends AbstractTestApi @@ -13,23 +14,23 @@ public function testList() $this->mockRequest([ 'path' => '/projects/2/labels', 'method' => 'get', - 'response' => '{ - "data": [ - { - "data": { - "id": 4, - "projectId": 2, - "title": "main" - } - } - ], - "pagination": [ - { - "offset": 0, - "limit": 0 - } - ] - }' + 'response' => json_encode([ + 'data' => [ + [ + 'data' => [ + 'id' => 4, + 'projectId' => 2, + 'title' => 'main', + ], + ], + ], + 'pagination' => [ + [ + 'offset' => 0, + 'limit' => 0, + ], + ], + ]), ]); $labels = $this->crowdin->label->list(2); @@ -42,13 +43,16 @@ public function testList() public function testGetAndUpdate() { - $this->mockRequestGet('/projects/2/labels/34', '{ - "data": { - "id": 34, - "projectId": 2, - "title": "develop-master" - } - }'); + $this->mockRequestGet( + '/projects/2/labels/34', + json_encode([ + 'data' => [ + 'id' => 34, + 'projectId' => 2, + 'title' => 'develop-master', + ], + ]) + ); $label = $this->crowdin->label->get(2, 34); @@ -57,13 +61,16 @@ public function testGetAndUpdate() $label->setTitle('edit-test'); - $this->mockRequestPatch('/projects/2/labels/34', '{ - "data": { - "id": 34, - "projectId": 2, - "name": "edit-test" - } - }'); + $this->mockRequestPatch( + '/projects/2/labels/34', + json_encode([ + 'data' => [ + 'id' => 34, + 'projectId' => 2, + 'name' => 'edit-test', + ], + ]) + ); $this->crowdin->label->update($label); $this->assertInstanceOf(Label::class, $label); @@ -81,13 +88,13 @@ public function testCreate() 'path' => '/projects/2/labels', 'method' => 'post', 'body' => json_encode($params), - 'response' => '{ - "data": { - "id": 34, - "projectId": 2, - "title": "develop-master" - } - }' + 'response' => json_encode([ + 'data' => [ + 'id' => 34, + 'projectId' => 2, + 'title' => 'develop-master', + ], + ]), ]); $label = $this->crowdin->label->create(2, $params); @@ -111,46 +118,7 @@ public function testAssignScreenshots() 'path' => '/projects/2/labels/34/screenshots', 'method' => 'post', 'body' => json_encode($params), - 'response' => '{ - "data": [ - { - "data": { - "id": 2, - "userId": 6, - "url": "https://production-enterprise-screenshots.downloads.crowdin.com/992000002/6/2/middle.jpg?X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIGJKLQV66ZXPMMEA%2F20190923%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20190923T093016Z&X-Amz-SignedHeaders=host&X-Amz-Expires=120&X-Amz-Signature=8df06f57594f7d1804b7c037629f6916224415e9b935c4f6619fbe002fb25e73", - "name": "translate_with_siri.jpg", - "size": { - "width": 267, - "height": 176 - }, - "tagsCount": 0, - "tags": [ - { - "id": 98, - "screenshotId": 2, - "stringId": 2822, - "position": { - "x": 474, - "y": 147, - "width": 490, - "height": 99 - }, - "createdAt": "2019-09-23T09:35:31+00:00" - } - ], - "labels": [ - 1 - ], - "createdAt": "2019-09-23T09:29:19+00:00", - "updatedAt": "2019-09-23T09:29:19+00:00" - } - } - ], - "pagination": { - "offset": 0, - "limit": 25 - } - }' + 'response' => $this->screenshotsResponse(), ]); $screenshots = $this->crowdin->label->assignScreenshots(2, 34, $params); @@ -163,53 +131,13 @@ public function testAssignScreenshots() public function testUnassignScreenshots() { $params = [ - 'screenshotIds' => ['2'], + 'screenshotIds' => [2, 3], ]; $this->mockRequest([ - 'path' => '/projects/2/labels/34/screenshots', + 'path' => '/projects/2/labels/34/screenshots?screenshotIds=2%2C3', 'method' => 'delete', - 'body' => json_encode($params), - 'response' => '{ - "data": [ - { - "data": { - "id": 2, - "userId": 6, - "url": "https://production-enterprise-screenshots.downloads.crowdin.com/992000002/6/2/middle.jpg?X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIGJKLQV66ZXPMMEA%2F20190923%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20190923T093016Z&X-Amz-SignedHeaders=host&X-Amz-Expires=120&X-Amz-Signature=8df06f57594f7d1804b7c037629f6916224415e9b935c4f6619fbe002fb25e73", - "name": "translate_with_siri.jpg", - "size": { - "width": 267, - "height": 176 - }, - "tagsCount": 0, - "tags": [ - { - "id": 98, - "screenshotId": 2, - "stringId": 2822, - "position": { - "x": 474, - "y": 147, - "width": 490, - "height": 99 - }, - "createdAt": "2019-09-23T09:35:31+00:00" - } - ], - "labels": [ - 1 - ], - "createdAt": "2019-09-23T09:29:19+00:00", - "updatedAt": "2019-09-23T09:29:19+00:00" - } - } - ], - "pagination": { - "offset": 0, - "limit": 25 - } - }' + 'response' => $this->screenshotsResponse(), ]); $screenshots = $this->crowdin->label->unassignScreenshots(2, 34, $params); @@ -218,4 +146,110 @@ public function testUnassignScreenshots() $this->assertInstanceOf(Screenshot::class, $screenshots[0]); $this->assertEquals(2, $screenshots[0]->getId()); } + + private function screenshotsResponse(): string + { + return json_encode([ + 'data' => [ + [ + 'data' => [ + 'id' => 2, + 'userId' => 6, + 'url' => 'https://production-enterprise-screenshots.downloads.crowdin.com/992000002/6/2/middle.jpg?X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIGJKLQV66ZXPMMEA%2F20190923%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20190923T093016Z&X-Amz-SignedHeaders=host&X-Amz-Expires=120&X-Amz-Signature=8df06f57594f7d1804b7c037629f6916224415e9b935c4f6619fbe002fb25e73', + 'name' => 'translate_with_siri.jpg', + 'size' => [ + 'width' => 267, + 'height' => 176, + ], + 'tagsCount' => 0, + 'tags' => [ + [ + 'id' => 98, + 'screenshotId' => 2, + 'stringId' => 2822, + 'position' => [ + 'x' => 474, + 'y' => 147, + 'width' => 490, + 'height' => 99, + ], + 'createdAt' => '2019-09-23T09:35:31+00:00', + ], + ], + 'labels' => [1], + 'createdAt' => '2019-09-23T09:29:19+00:00', + 'updatedAt' => '2019-09-23T09:29:19+00:00', + ], + ], + ], + 'pagination' => [ + 'offset' => 0, + 'limit' => 25, + ], + ]); + } + + public function testAssignStrings(): void + { + $params = ['stringIds' => [2814, 2815]]; + + $this->mockRequest([ + 'path' => '/projects/2/labels/34/strings', + 'method' => 'post', + 'body' => json_encode($params), + 'response' => $this->stringsResponse(), + ]); + + $strings = $this->crowdin->label->assignStrings(2, 34, $params); + + $this->assertInstanceOf(ModelCollection::class, $strings); + $this->assertCount(1, $strings); + $this->assertInstanceOf(SourceString::class, $strings[0]); + $this->assertEquals(2814, $strings[0]->getId()); + } + + public function testUnassignStrings(): void + { + $this->mockRequest([ + 'path' => '/projects/2/labels/34/strings?stringIds=2814%2C2815', + 'method' => 'delete', + 'response' => $this->stringsResponse(), + ]); + + $strings = $this->crowdin->label->unassignStrings(2, 34, ['stringIds' => [2814, 2815]]); + + $this->assertInstanceOf(ModelCollection::class, $strings); + $this->assertCount(1, $strings); + $this->assertInstanceOf(SourceString::class, $strings[0]); + } + + public function testUnassignStringsAcceptsCommaSeparatedString(): void + { + $this->mockRequest([ + 'path' => '/projects/2/labels/34/strings?stringIds=2814%2C2815', + 'method' => 'delete', + 'response' => $this->stringsResponse(), + ]); + + $this->crowdin->label->unassignStrings(2, 34, ['stringIds' => '2814,2815']); + } + + private function stringsResponse(): string + { + return json_encode([ + 'data' => [ + [ + 'data' => [ + 'id' => 2814, + 'projectId' => 2, + 'identifier' => 'name', + 'text' => 'Not all videos are shown to users.', + 'type' => 'text', + 'labelIds' => [34], + ], + ], + ], + 'pagination' => ['offset' => 0, 'limit' => 25], + ]); + } } diff --git a/tests/CrowdinApiClient/Api/TranslationApiTest.php b/tests/CrowdinApiClient/Api/TranslationApiTest.php index 173ede05..ec07ed09 100644 --- a/tests/CrowdinApiClient/Api/TranslationApiTest.php +++ b/tests/CrowdinApiClient/Api/TranslationApiTest.php @@ -8,6 +8,7 @@ use CrowdinApiClient\Model\PreTranslationReport; use CrowdinApiClient\Model\PreTranslationReportFile; use CrowdinApiClient\Model\PreTranslationReportLanguage; +use CrowdinApiClient\Model\QaCheck; use CrowdinApiClient\Model\TranslationAlignment; use CrowdinApiClient\Model\TranslationImport; use CrowdinApiClient\Model\TranslationImportReport; @@ -712,4 +713,44 @@ public function testUpdatePreTranslations(): void $this->assertEquals('9e7de270-4f83-41cb-b606-2f90631f26e2', $preTranslations[0]->getIdentifier()); $this->assertEquals('canceled', $preTranslations[0]->getStatus()); } + + public function testValidateQaChecks(): void + { + $data = [ + ['stringId' => 35434, 'languageId' => 'uk', 'text' => 'Перша локалзація.', 'pluralCategoryName' => 'few'], + ]; + + $this->mockRequest([ + 'path' => '/projects/2/translations/validate-qa-checks', + 'method' => 'post', + 'body' => json_encode($data), + 'response' => json_encode([ + 'data' => [ + [ + 'data' => [ + 'stringId' => 35434, + 'languageId' => 'uk', + 'category' => 'spellcheck', + 'categoryDescription' => 'Spelling', + 'validation' => 'spellcheck', + 'validationDescription' => 'Misspelling', + 'pluralId' => -1, + 'pluralCategoryName' => 'few', + 'text' => "Spellcheck failed for the following word: 'локалзація'.", + 'translation' => 'Перша локалзація.', + ], + ], + ], + 'pagination' => ['offset' => 0, 'limit' => 25], + ]), + ]); + + $qaChecks = $this->crowdin->translation->validateQaChecks(2, $data); + + $this->assertInstanceOf(ModelCollection::class, $qaChecks); + $this->assertCount(1, $qaChecks); + $this->assertInstanceOf(QaCheck::class, $qaChecks[0]); + $this->assertEquals('spellcheck', $qaChecks[0]->getCategory()); + $this->assertEquals('Перша локалзація.', $qaChecks[0]->getTranslation()); + } } diff --git a/tests/CrowdinApiClient/Api/TranslationMemoryApiTest.php b/tests/CrowdinApiClient/Api/TranslationMemoryApiTest.php index d518f484..8a96cfda 100644 --- a/tests/CrowdinApiClient/Api/TranslationMemoryApiTest.php +++ b/tests/CrowdinApiClient/Api/TranslationMemoryApiTest.php @@ -17,34 +17,31 @@ public function testList() $this->mockRequest([ 'path' => '/tms', 'method' => 'get', - 'response' => '{ - "data": [ - { - "data": { - "id": 4, - "groupId": 2, - "name": "Knowledge Base\'s TM", - "languageIds": [ - "el" + 'response' => json_encode([ + 'data' => [ + [ + 'data' => [ + 'id' => 4, + 'groupId' => 2, + 'name' => 'Knowledge Base\'s TM', + 'languageIds' => ['el'], + 'segmentsCount' => 21, + 'defaultProjectIds' => [0], + 'projectIds' => [2], + ], ], - "segmentsCount": 21, - "defaultProjectIds": [0], - "projectIds": [ - 2 - ] - } - } - ], - "pagination": [ - { - "offset": 0, - "limit": 25 - } - ] - }' + ], + 'pagination' => [ + [ + 'offset' => 0, + 'limit' => 25, + ], + ], + ]), ]); $translationMemories = $this->crowdin->translationMemory->list(); + $this->assertInstanceOf(ModelCollection::class, $translationMemories); $this->assertCount(1, $translationMemories); $this->assertInstanceOf(TranslationMemory::class, $translationMemories[0]); @@ -61,74 +58,71 @@ public function testCreate() 'path' => '/tms', 'method' => 'post', 'body' => json_encode($params), - 'response' => '{ - "data": { - "id": 4, - "groupId": 2, - "name": "Knowledge Base\'s TM", - "languageIds": [ - "el" - ], - "segmentsCount": 21, - "defaultProjectIds": [0], - "projectIds": [ - 2 + 'response' => json_encode([ + 'data' => [ + 'id' => 4, + 'groupId' => 2, + 'name' => 'Knowledge Base\'s TM', + 'languageIds' => ['el'], + 'segmentsCount' => 21, + 'defaultProjectIds' => [0], + 'projectIds' => [2], + 'createdAt' => '2019-09-23T09:04:29+00:00', ], - "createdAt":"2019-09-23T09:04:29+00:00" - } - }' + ]), ]); $translationMemory = $this->crowdin->translationMemory->create($params); + $this->assertInstanceOf(TranslationMemory::class, $translationMemory); $this->assertEquals(4, $translationMemory->getId()); } public function testGetAndUpdate() { - $this->mockRequestGet('/tms/4', '{ - "data": { - "id": 4, - "groupId": 2, - "name": "Knowledge Base\'s TM", - "languageIds": [ - "el" + $this->mockRequestGet( + '/tms/4', + json_encode([ + 'data' => [ + 'id' => 4, + 'groupId' => 2, + 'name' => 'Knowledge Base\'s TM', + 'languageIds' => ['el'], + 'segmentsCount' => 21, + 'defaultProjectIds' => [0], + 'projectIds' => [2], ], - "segmentsCount": 21, - "defaultProjectIds": [0], - "projectIds": [ - 2 - ] - } - }'); + ]) + ); $translationMemory = $this->crowdin->translationMemory->get(4); + $this->assertInstanceOf(TranslationMemory::class, $translationMemory); $this->assertEquals(4, $translationMemory->getId()); - $this->mockRequestPatch('/tms/4', '{ - "data": { - "id": 4, - "groupId": 2, - "name": "test edit", - "languageIds": [ - "el" + $this->mockRequestPatch( + '/tms/4', + json_encode([ + 'data' => [ + 'id' => 4, + 'groupId' => 2, + 'name' => 'test edit', + 'languageIds' => ['el'], + 'segmentsCount' => 21, + 'defaultProjectIds' => [0], + 'projectIds' => [2], ], - "segmentsCount": 21, - "defaultProjectIds": [0], - "projectIds": [ - 2 - ] - } - }'); + ]) + ); $translationMemory->setName('test edit'); - $translationMemory = $this->crowdin->translationMemory->update($translationMemory); + $this->assertInstanceOf(TranslationMemory::class, $translationMemory); $this->assertEquals(4, $translationMemory->getId()); $this->assertEquals('test edit', $translationMemory->getName()); } + public function testDelete() { $this->mockRequestDelete('/tms/4'); @@ -137,16 +131,42 @@ public function testDelete() public function testDownload() { - $this->mockRequestGet('/tms/4/exports/1/download', '{ - "data": { - "url": "https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff?response-content-disposition=attachment%3B%20filename%3D%22APP.xliff%22&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIGJKLQV66ZXPMMEA%2F20190920%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20190920T093121Z&X-Amz-SignedHeaders=host&X-Amz-Expires=3600&X-Amz-Signature=439ebd69a1b7e4c23e6d17891a491c94f832e0c82e4692dedb35a6cd1e624b62", - "expireIn": "2019-09-20T10:31:21+00:00" - } - }'); + $this->mockRequestGet('/tms/4/exports/1/download', json_encode([ + 'data' => [ + 'url' => 'https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff?response-content-disposition=attachment%3B%20filename%3D%22APP.xliff%22&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIGJKLQV66ZXPMMEA%2F20190920%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20190920T093121Z&X-Amz-SignedHeaders=host&X-Amz-Expires=3600&X-Amz-Signature=439ebd69a1b7e4c23e6d17891a491c94f832e0c82e4692dedb35a6cd1e624b62', + 'expireIn' => '2019-09-20T10:31:21+00:00', + ], + ])); $downloadFile = $this->crowdin->translationMemory->download(4, 1); + $this->assertInstanceOf(DownloadFile::class, $downloadFile); - $this->assertEquals('https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff?response-content-disposition=attachment%3B%20filename%3D%22APP.xliff%22&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIGJKLQV66ZXPMMEA%2F20190920%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20190920T093121Z&X-Amz-SignedHeaders=host&X-Amz-Expires=3600&X-Amz-Signature=439ebd69a1b7e4c23e6d17891a491c94f832e0c82e4692dedb35a6cd1e624b62', $downloadFile->getUrl()); + $this->assertEquals( + 'https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff?response-content-disposition=attachment%3B%20filename%3D%22APP.xliff%22&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIGJKLQV66ZXPMMEA%2F20190920%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20190920T093121Z&X-Amz-SignedHeaders=host&X-Amz-Expires=3600&X-Amz-Signature=439ebd69a1b7e4c23e6d17891a491c94f832e0c82e4692dedb35a6cd1e624b62', + $downloadFile->getUrl() + ); + } + + private function exportResponse(): string + { + return json_encode([ + 'data' => [ + 'identifier' => '50fb3506-4127-4ba8-8296-f97dc7e3e0c3', + 'status' => 'finished', + 'progress' => 100, + 'attributes' => [ + 'sourceLanguageId' => 'en', + 'targetLanguageId' => 'de', + 'format' => 'csv', + 'tmId' => 4, + 'userId' => 6, + ], + 'createdAt' => '2019-09-23T11:26:54+00:00', + 'updatedAt' => '2019-09-23T11:26:54+00:00', + 'startedAt' => '2019-09-23T11:26:54+00:00', + 'finishedAt' => '2019-09-23T11:26:54+00:00', + ], + ]); } public function testExport() @@ -160,60 +180,28 @@ public function testExport() 'path' => '/tms/4/exports', 'method' => 'post', 'body' => json_encode($params), - 'response' => '{ - "data": { - "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", - "status": "finished", - "progress": 100, - "attributes": { - "sourceLanguageId": "en", - "targetLanguageId": "de", - "format": "csv", - "tmId": 4, - "userId": 6 - }, - "createdAt": "2019-09-23T11:26:54+00:00", - "updatedAt": "2019-09-23T11:26:54+00:00", - "startedAt": "2019-09-23T11:26:54+00:00", - "finishedAt": "2019-09-23T11:26:54+00:00" - } - }' + 'response' => $this->exportResponse(), ]); $export = $this->crowdin->translationMemory->export(4, $params); + $this->assertInstanceOf(TranslationMemoryExport::class, $export); $this->assertEquals('50fb3506-4127-4ba8-8296-f97dc7e3e0c3', $export->getIdentifier()); } public function testGetExport() { - $this->mockRequestGet('/tms/4/exports/50fb3506-4127-4ba8-8296-f97dc7e3e0c3', '{ - "data": { - "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", - "status": "finished", - "progress": 100, - "attributes": { - "sourceLanguageId": "en", - "targetLanguageId": "de", - "format": "csv", - "tmId": 4, - "userId": 6 - }, - "createdAt": "2019-09-23T11:26:54+00:00", - "updatedAt": "2019-09-23T11:26:54+00:00", - "startedAt": "2019-09-23T11:26:54+00:00", - "finishedAt": "2019-09-23T11:26:54+00:00" - } - }'); + $this->mockRequestGet('/tms/4/exports/50fb3506-4127-4ba8-8296-f97dc7e3e0c3', $this->exportResponse()); $export = $this->crowdin->translationMemory->checkExportStatus(4, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + $this->assertInstanceOf(TranslationMemoryExport::class, $export); $this->assertEquals('50fb3506-4127-4ba8-8296-f97dc7e3e0c3', $export->getIdentifier()); } public function testImport() { - $expected_body = [ + $expectedBody = [ 'storageId' => 0, 'firstLineContainsHeader' => false, 'scheme' => @@ -234,30 +222,8 @@ public function testImport() $this->mockRequest([ 'path' => '/tms/4/imports', 'method' => 'post', - 'body' => json_encode($expected_body), - 'response' => '{ - "data": { - "identifier": "b5215a34-1305-4b21-8054-fc2eb252842f", - "status": "created", - "progress": 0, - "attributes": { - "tmId": 10, - "storageId": 28, - "firstLineContainsHeader": 10, - "scheme": { - "phraseEn": 52, - "phraseEl": 52 - }, - "organizationId": 200000299, - "progressKey": "import_tm_progress_10", - "userId": 6 - }, - "createdAt": "2019-09-23T11:51:08+00:00", - "updatedAt": "2019-09-23T11:51:08+00:00", - "startedAt": "string", - "finishedAt": "string" - } - }' + 'body' => json_encode($expectedBody), + 'response' => $this->importResponse(), ]); $translationMemoryImport = $this->crowdin->translationMemory->import(4, 0, false, $params); @@ -267,35 +233,44 @@ public function testImport() public function testCheckImportStatus() { - $this->mockRequestGet('/tms/4/imports/b5215a34-1305-4b21-8054-fc2eb252842f', '{ - "data": { - "identifier": "b5215a34-1305-4b21-8054-fc2eb252842f", - "status": "created", - "progress": 0, - "attributes": { - "tmId": 10, - "storageId": 28, - "firstLineContainsHeader": 10, - "scheme": { - "phraseEn": 52, - "phraseEl": 52 - }, - "organizationId": 200000299, - "progressKey": "import_tm_progress_10", - "userId": 6 - }, - "createdAt": "2019-09-23T11:51:08+00:00", - "updatedAt": "2019-09-23T11:51:08+00:00", - "startedAt": "string", - "finishedAt": "string" - } - }'); - - $translationMemoryImport = $this->crowdin->translationMemory->checkImportStatus(4, 'b5215a34-1305-4b21-8054-fc2eb252842f'); + $this->mockRequestGet('/tms/4/imports/b5215a34-1305-4b21-8054-fc2eb252842f', $this->importResponse()); + + $translationMemoryImport = $this->crowdin->translationMemory->checkImportStatus( + 4, + 'b5215a34-1305-4b21-8054-fc2eb252842f' + ); + $this->assertInstanceOf(TranslationMemoryImport::class, $translationMemoryImport); $this->assertEquals('b5215a34-1305-4b21-8054-fc2eb252842f', $translationMemoryImport->getIdentifier()); } + private function importResponse(): string + { + return json_encode([ + 'data' => [ + 'identifier' => 'b5215a34-1305-4b21-8054-fc2eb252842f', + 'status' => 'created', + 'progress' => 0, + 'attributes' => [ + 'tmId' => 10, + 'storageId' => 28, + 'firstLineContainsHeader' => 10, + 'scheme' => [ + 'phraseEn' => 52, + 'phraseEl' => 52, + ], + 'organizationId' => 200000299, + 'progressKey' => 'import_tm_progress_10', + 'userId' => 6, + ], + 'createdAt' => '2019-09-23T11:51:08+00:00', + 'updatedAt' => '2019-09-23T11:51:08+00:00', + 'startedAt' => 'string', + 'finishedAt' => 'string', + ], + ]); + } + public function testClear() { $this->mockRequestDelete('/tms/4/segments'); @@ -321,28 +296,28 @@ public function testConcordance(): void 'path' => '/projects/4/tms/concordance', 'method' => 'post', 'body' => json_encode($params), - 'response' => '{ - "data": [ - { - "data": { - "tm": { - "id": 4, - "name": "Knowledge Base TM" - }, - "recordId": 34, - "source": "Welcome!", - "target": "Ласкаво просимо!", - "relevant": 100, - "substituted": "62→100", - "updatedAt": "2022-09-28T12:29:34+00:00" - } - } - ], - "pagination": { - "offset": 0, - "limit": 25 - } - }', + 'response' => json_encode([ + 'data' => [ + [ + 'data' => [ + 'tm' => [ + 'id' => 4, + 'name' => 'Knowledge Base TM', + ], + 'recordId' => 34, + 'source' => 'Welcome!', + 'target' => 'Ласкаво просимо!', + 'relevant' => 100, + 'substituted' => '62→100', + 'updatedAt' => '2022-09-28T12:29:34+00:00', + ], + ], + ], + 'pagination' => [ + 'offset' => 0, + 'limit' => 25, + ], + ]), ]); /** @var TranslationMemoryConcordance[] $concordance */ @@ -402,31 +377,31 @@ public function testListSegments() $this->mockRequest([ 'path' => '/tms/4/segments', 'method' => 'get', - 'response' => '{ - "data": [ - { - "data": { - "id": 4, - "records": [ - { - "id": 1, - "languageId": "uk", - "text": "Перекладений текст", - "usageCount": 13, - "createdBy": 1, - "updatedBy": 1, - "createdAt": "2019-09-16T13:48:04+00:00", - "updatedAt": "2019-09-16T13:48:04+00:00" - } - ] - } - } - ], - "pagination": { - "offset": 0, - "limit": 25 - } - }' + 'response' => json_encode([ + 'data' => [ + [ + 'data' => [ + 'id' => 4, + 'records' => [ + [ + 'id' => 1, + 'languageId' => 'uk', + 'text' => 'Перекладений текст', + 'usageCount' => 13, + 'createdBy' => 1, + 'updatedBy' => 1, + 'createdAt' => '2019-09-16T13:48:04+00:00', + 'updatedAt' => '2019-09-16T13:48:04+00:00', + ], + ], + ], + ], + ], + 'pagination' => [ + 'offset' => 0, + 'limit' => 25, + ], + ]), ]); $segments = $this->crowdin->translationMemory->listSegments(4); @@ -438,23 +413,26 @@ public function testListSegments() public function testGetSegment() { - $this->mockRequestGet('/tms/4/segments/1', '{ - "data": { - "id": 4, - "records": [ - { - "id": 1, - "languageId": "uk", - "text": "Перекладений текст", - "usageCount": 13, - "createdBy": 1, - "updatedBy": 1, - "createdAt": "2019-09-16T13:48:04+00:00", - "updatedAt": "2019-09-16T13:48:04+00:00" - } - ] - } - }'); + $this->mockRequestGet( + '/tms/4/segments/1', + json_encode([ + 'data' => [ + 'id' => 4, + 'records' => [ + [ + 'id' => 1, + 'languageId' => 'uk', + 'text' => 'Перекладений текст', + 'usageCount' => 13, + 'createdBy' => 1, + 'updatedBy' => 1, + 'createdAt' => '2019-09-16T13:48:04+00:00', + 'updatedAt' => '2019-09-16T13:48:04+00:00', + ], + ], + ], + ]) + ); $segment = $this->crowdin->translationMemory->getSegment(4, 1); $this->assertInstanceOf(TranslationMemorySegment::class, $segment); @@ -476,26 +454,27 @@ public function testCreateSegment() 'path' => '/tms/4/segments', 'method' => 'post', 'body' => json_encode($params), - 'response' => '{ - "data": { - "id": 4, - "records": [ - { - "id": 1, - "languageId": "uk", - "text": "Перекладений текст", - "usageCount": 13, - "createdBy": 1, - "updatedBy": 1, - "createdAt": "2019-09-16T13:48:04+00:00", - "updatedAt": "2019-09-16T13:48:04+00:00" - } - ] - } - }' + 'response' => json_encode([ + 'data' => [ + 'id' => 4, + 'records' => [ + [ + 'id' => 1, + 'languageId' => 'uk', + 'text' => 'Перекладений текст', + 'usageCount' => 13, + 'createdBy' => 1, + 'updatedBy' => 1, + 'createdAt' => '2019-09-16T13:48:04+00:00', + 'updatedAt' => '2019-09-16T13:48:04+00:00', + ], + ], + ], + ]), ]); $segment = $this->crowdin->translationMemory->createSegment(4, $params); + $this->assertInstanceOf(TranslationMemorySegment::class, $segment); $this->assertEquals(4, $segment->getId()); } @@ -579,4 +558,86 @@ public function testDeleteSegment() $this->mockRequestDelete('/tms/4/segments/1'); $this->crowdin->translationMemory->deleteSegment(4, 1); } + + public function testUpdateSegment(): void + { + $data = [ + ['op' => 'add', 'path' => '/records/-', 'value' => ['languageId' => 'uk', 'text' => 'Перекладений текст']], + ['op' => 'remove', 'path' => '/records/5'], + ]; + + $this->mockRequest([ + 'path' => '/tms/4/segments/1', + 'method' => 'patch', + 'body' => json_encode($data), + 'response' => $this->segmentResponse(), + ]); + + $segment = $this->crowdin->translationMemory->updateSegment(4, 1, $data); + + $this->assertInstanceOf(TranslationMemorySegment::class, $segment); + $this->assertEquals(1, $segment->getId()); + $this->assertEquals(7, $segment->getRecords()[0]->getId()); + } + + public function testCreateSegmentRecords(): void + { + $data = ['records' => [['languageId' => 'uk', 'text' => 'Перекладений текст']]]; + + $this->mockRequest([ + 'path' => '/tms/4/segments/1/records', + 'method' => 'post', + 'body' => json_encode($data), + 'response' => $this->segmentResponse(), + ]); + + $segment = $this->crowdin->translationMemory->createSegmentRecords(4, 1, $data); + + $this->assertInstanceOf(TranslationMemorySegment::class, $segment); + $this->assertEquals('uk', $segment->getRecords()[0]->getLanguageId()); + } + + public function testUpdateSegmentRecord(): void + { + $data = [['op' => 'replace', 'path' => '/text', 'value' => 'Перекладений текст']]; + + $this->mockRequest([ + 'path' => '/tms/4/segments/1/records/7', + 'method' => 'patch', + 'body' => json_encode($data), + 'response' => $this->segmentResponse(), + ]); + + $segment = $this->crowdin->translationMemory->updateSegmentRecord(4, 1, 7, $data); + + $this->assertInstanceOf(TranslationMemorySegment::class, $segment); + $this->assertEquals('Перекладений текст', $segment->getRecords()[0]->getText()); + } + + private function segmentResponse(): string + { + return json_encode([ + 'data' => [ + 'id' => 1, + 'records' => [ + [ + 'id' => 7, + 'languageId' => 'uk', + 'text' => 'Перекладений текст', + 'usageCount' => 0, + 'createdBy' => 1, + 'updatedBy' => 1, + 'createdAt' => '2023-09-20T11:34:40+00:00', + 'updatedAt' => '2023-09-20T11:34:40+00:00', + ], + ], + ], + ]); + } + + public function testDeleteSegmentRecord(): void + { + $this->mockRequestDelete('/tms/4/segments/1/records/7'); + $this->crowdin->translationMemory->deleteSegmentRecord(4, 1, 7); + } } diff --git a/tests/CrowdinApiClient/Model/QaCheckTest.php b/tests/CrowdinApiClient/Model/QaCheckTest.php index 36bd4813..c1fe324c 100644 --- a/tests/CrowdinApiClient/Model/QaCheckTest.php +++ b/tests/CrowdinApiClient/Model/QaCheckTest.php @@ -41,5 +41,18 @@ public function testLoadAndGetData() $this->assertEquals($this->data['validationDescription'], $this->QaCheck->getValidationDescription()); $this->assertEquals($this->data['pluralId'], $this->QaCheck->getPluralId()); $this->assertEquals($this->data['text'], $this->QaCheck->getText()); + $this->assertNull($this->QaCheck->getPluralCategoryName()); + $this->assertNull($this->QaCheck->getTranslation()); + } + + public function testLoadValidateQaChecksData(): void + { + $qaCheck = new QaCheck(array_merge($this->data, [ + 'pluralCategoryName' => 'few', + 'translation' => 'Перша локалзація.', + ])); + + $this->assertEquals('few', $qaCheck->getPluralCategoryName()); + $this->assertEquals('Перша локалзація.', $qaCheck->getTranslation()); } } From 920c763b9cc9de2b785f8e055863572ad3f0e559 Mon Sep 17 00:00:00 2001 From: Bohdan Bobko <172895427+bhdnb@users.noreply.github.com> Date: Wed, 30 Sep 2026 17:49:36 +0300 Subject: [PATCH 4/9] feat: add AI fine-tuning, provider models, request log exports and usage --- src/CrowdinApiClient/Api/AiApi.php | 276 ++++ src/CrowdinApiClient/Api/Enterprise/AiApi.php | 262 ++++ .../Api/TranslationMemoryApi.php | 56 - .../Model/AiFineTuningDataset.php | 9 + .../Model/AiFineTuningEvent.php | 70 + .../Model/AiFineTuningJob.php | 9 + src/CrowdinApiClient/Model/AiMemberUsage.php | 80 ++ .../Model/AiProviderModel.php | 72 + .../Model/AiRequestLogExport.php | 23 + .../Model/AiSupportedModel.php | 158 +++ src/CrowdinApiClient/Model/BaseModel.php | 32 + src/CrowdinApiClient/Model/Project.php | 4 +- .../Model/ProjectAiSettings.php | 53 + src/CrowdinApiClient/Model/QaCheck.php | 8 +- .../Model/ReportSettingsTemplate.php | 8 +- src/CrowdinApiClient/Model/Screenshot.php | 2 +- tests/CrowdinApiClient/Api/AiApiTest.php | 1218 ++++++++++++----- .../Api/Enterprise/AiApiTest.php | 1198 +++++++++++----- .../Api/TranslationMemoryApiTest.php | 79 +- .../Model/AiFineTuningEventTest.php | 37 + .../Model/AiMemberUsageTest.php | 47 + .../Model/AiProviderModelTest.php | 55 + .../Model/AiRequestLogExportTest.php | 48 + .../Model/AiSupportedModelTest.php | 58 + .../Model/ProjectAiSettingsTest.php | 36 + 25 files changed, 3043 insertions(+), 855 deletions(-) create mode 100644 src/CrowdinApiClient/Model/AiFineTuningDataset.php create mode 100644 src/CrowdinApiClient/Model/AiFineTuningEvent.php create mode 100644 src/CrowdinApiClient/Model/AiFineTuningJob.php create mode 100644 src/CrowdinApiClient/Model/AiMemberUsage.php create mode 100644 src/CrowdinApiClient/Model/AiRequestLogExport.php create mode 100644 src/CrowdinApiClient/Model/AiSupportedModel.php create mode 100644 src/CrowdinApiClient/Model/ProjectAiSettings.php create mode 100644 tests/CrowdinApiClient/Model/AiFineTuningEventTest.php create mode 100644 tests/CrowdinApiClient/Model/AiMemberUsageTest.php create mode 100644 tests/CrowdinApiClient/Model/AiProviderModelTest.php create mode 100644 tests/CrowdinApiClient/Model/AiRequestLogExportTest.php create mode 100644 tests/CrowdinApiClient/Model/AiSupportedModelTest.php create mode 100644 tests/CrowdinApiClient/Model/ProjectAiSettingsTest.php diff --git a/src/CrowdinApiClient/Api/AiApi.php b/src/CrowdinApiClient/Api/AiApi.php index 5c3ba373..030b84b1 100644 --- a/src/CrowdinApiClient/Api/AiApi.php +++ b/src/CrowdinApiClient/Api/AiApi.php @@ -3,6 +3,10 @@ namespace CrowdinApiClient\Api; use CrowdinApiClient\Model\AiFileTranslation; +use CrowdinApiClient\Model\AiFineTuningDataset; +use CrowdinApiClient\Model\AiFineTuningEvent; +use CrowdinApiClient\Model\AiFineTuningJob; +use CrowdinApiClient\Model\AiMemberUsage; use CrowdinApiClient\Model\AiPrompt; use CrowdinApiClient\Model\AiPromptCompletion; use CrowdinApiClient\Model\AiProvider; @@ -10,10 +14,13 @@ use CrowdinApiClient\Model\AiProxyChatCompletion; use CrowdinApiClient\Model\AiReport; use CrowdinApiClient\Model\AiRequestLog; +use CrowdinApiClient\Model\AiRequestLogExport; use CrowdinApiClient\Model\AiSettings; use CrowdinApiClient\Model\AiSnippet; +use CrowdinApiClient\Model\AiSupportedModel; use CrowdinApiClient\Model\AiTranslation; use CrowdinApiClient\Model\DownloadFile; +use CrowdinApiClient\Model\ProjectAiSettings; use CrowdinApiClient\ModelCollection; /** @@ -580,4 +587,273 @@ public function deleteSnippet(int $userId, int $aiSnippetId) $path = sprintf('users/%d/ai/settings/snippets/%d', $userId, $aiSnippetId); return $this->_delete($path); } + + /** + * Get Project AI Settings + * @link https://developer.crowdin.com/api/v2/#operation/api.projects.ai.settings.get API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.ai.settings.get API Documentation Enterprise + * + * @param int $projectId + * @return ProjectAiSettings|null + */ + public function getProjectSettings(int $projectId): ?ProjectAiSettings + { + $path = sprintf('projects/%d/ai/settings', $projectId); + return $this->_get($path, ProjectAiSettings::class); + } + + /** + * Generate AI Prompt Fine-Tuning Dataset + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.prompts.fine-tuning.datasets.post API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.fine-tuning.datasets.post API Documentation Enterprise + * + * @param int $userId + * @param int $aiPromptId + * @param array $data + * int[] $data[projectIds]
+ * int[] $data[tmIds]
+ * string $data[purpose] Enum: "training" "validation"
+ * string $data[dateFrom]
+ * string $data[dateTo]
+ * integer $data[maxFileSize]
+ * integer $data[minExamplesCount]
+ * integer $data[maxExamplesCount] + * @return AiFineTuningDataset|null + */ + public function generateFineTuningDataset(int $userId, int $aiPromptId, array $data): ?AiFineTuningDataset + { + $path = sprintf('users/%d/ai/prompts/%d/fine-tuning/datasets', $userId, $aiPromptId); + return $this->_post($path, AiFineTuningDataset::class, $data); + } + + /** + * Get AI Prompt Fine-Tuning Dataset Generation Status + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.prompts.fine-tuning.datasets.get API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.fine-tuning.datasets.get API Documentation Enterprise + * + * @param int $userId + * @param int $aiPromptId + * @param string $jobIdentifier + * @return AiFineTuningDataset|null + */ + public function getFineTuningDataset(int $userId, int $aiPromptId, string $jobIdentifier): ?AiFineTuningDataset + { + $path = sprintf('users/%d/ai/prompts/%d/fine-tuning/datasets/%s', $userId, $aiPromptId, $jobIdentifier); + return $this->_get($path, AiFineTuningDataset::class); + } + + /** + * Download AI Prompt Fine-Tuning Dataset + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.prompts.fine-tuning.datasets.download.get API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.fine-tuning.datasets.download.get API Documentation Enterprise + * + * @param int $userId + * @param int $aiPromptId + * @param string $jobIdentifier + * @return DownloadFile|null + */ + public function downloadFineTuningDataset(int $userId, int $aiPromptId, string $jobIdentifier): ?DownloadFile + { + $path = sprintf('users/%d/ai/prompts/%d/fine-tuning/datasets/%s/download', $userId, $aiPromptId, $jobIdentifier); + return $this->_get($path, DownloadFile::class); + } + + /** + * List AI Prompt Fine-Tuning Jobs + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.prompts.fine-tuning.jobs.getMany API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.fine-tuning.jobs.getMany API Documentation Enterprise + * + * @param int $userId + * @param array $params + * string $params[statuses] Comma-separated list of statuses
+ * string $params[orderBy]
+ * integer $params[limit]
+ * integer $params[offset] + * @return ModelCollection + */ + public function listFineTuningJobs(int $userId, array $params = []): ModelCollection + { + $path = sprintf('users/%d/ai/prompts/fine-tuning/jobs', $userId); + return $this->_list($path, AiFineTuningJob::class, $params); + } + + /** + * Create AI Prompt Fine-Tuning Job + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.prompts.fine-tuning.jobs.post API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.fine-tuning.jobs.post API Documentation Enterprise + * + * @param int $userId + * @param int $aiPromptId + * @param array $data + * boolean $data[dryRun]
+ * array $data[hyperparameters] batchSize, learningRateMultiplier, nEpochs
+ * array $data[trainingOptions] required. projectIds, tmIds, dateFrom, dateTo, maxFileSize, minExamplesCount, maxExamplesCount
+ * array $data[validationOptions] same keys as trainingOptions + * @return AiFineTuningJob|null + */ + public function createFineTuningJob(int $userId, int $aiPromptId, array $data): ?AiFineTuningJob + { + $path = sprintf('users/%d/ai/prompts/%d/fine-tuning/jobs', $userId, $aiPromptId); + return $this->_post($path, AiFineTuningJob::class, $data); + } + + /** + * Get AI Prompt Fine-Tuning Job Status + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.prompts.fine-tuning.jobs.get API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.fine-tuning.jobs.get API Documentation Enterprise + * + * @param int $userId + * @param int $aiPromptId + * @param string $jobIdentifier + * @return AiFineTuningJob|null + */ + public function getFineTuningJob(int $userId, int $aiPromptId, string $jobIdentifier): ?AiFineTuningJob + { + $path = sprintf('users/%d/ai/prompts/%d/fine-tuning/jobs/%s', $userId, $aiPromptId, $jobIdentifier); + return $this->_get($path, AiFineTuningJob::class); + } + + /** + * List AI Prompt Fine-Tuning Events + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.prompts.fine-tuning.jobs.events.getMany API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.fine-tuning.jobs.events.getMany API Documentation Enterprise + * + * @param int $userId + * @param int $aiPromptId + * @param string $jobIdentifier + * @return ModelCollection + */ + public function listFineTuningEvents(int $userId, int $aiPromptId, string $jobIdentifier): ModelCollection + { + $path = sprintf('users/%d/ai/prompts/%d/fine-tuning/jobs/%s/events', $userId, $aiPromptId, $jobIdentifier); + return $this->_list($path, AiFineTuningEvent::class); + } + + /** + * List AI Provider Models (across all providers) + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.providers.models.crowdin.getMany API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.providers.models.enterprise.getMany API Documentation Enterprise + * + * @param int $userId + * @return ModelCollection + */ + public function listAllProviderModels(int $userId): ModelCollection + { + $path = sprintf('users/%d/ai/providers/models', $userId); + return $this->_list($path, AiProviderModel::class); + } + + /** + * List Supported AI Provider Models + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.providers.supported-models.crowdin.getMany API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.providers.supported-models.enterprise.getMany API Documentation Enterprise + * + * @param int $userId + * @param array $params + * string $params[providerType]
+ * boolean $params[enabled]
+ * string $params[orderBy]
+ * integer $params[limit]
+ * integer $params[offset] + * @return ModelCollection + */ + public function listSupportedProviderModels(int $userId, array $params = []): ModelCollection + { + $path = sprintf('users/%d/ai/providers/supported-models', $userId); + return $this->_list($path, AiSupportedModel::class, $params); + } + + /** + * Export AI Request Logs + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.requestLogs.exports.post API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.requestLogs.exports.post API Documentation Enterprise + * + * @param int $userId + * @param array $data + * string $data[format] Enum: "csv"
+ * string $data[requestId]
+ * integer $data[projectId]
+ * integer $data[userId]
+ * integer $data[aiProviderId]
+ * string $data[model]
+ * string $data[sourceAction]
+ * string $data[promptAction]
+ * string[] $data[statuses]
+ * string $data[tokenName]
+ * string $data[oauthClientId]
+ * boolean $data[systemCredentials]
+ * boolean $data[isAutoTriggered]
+ * string $data[createdAfter]
+ * string $data[createdBefore] + * @return AiRequestLogExport|null + */ + public function exportRequestLogs(int $userId, array $data): ?AiRequestLogExport + { + $path = sprintf('users/%d/ai/request-logs/exports', $userId); + return $this->_post($path, AiRequestLogExport::class, $data); + } + + /** + * Check AI Request Logs Export Status + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.requestLogs.exports.get API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.requestLogs.exports.get API Documentation Enterprise + * + * @param int $userId + * @param string $exportId + * @return AiRequestLogExport|null + */ + public function getRequestLogsExport(int $userId, string $exportId): ?AiRequestLogExport + { + $path = sprintf('users/%d/ai/request-logs/exports/%s', $userId, $exportId); + return $this->_get($path, AiRequestLogExport::class); + } + + /** + * Download AI Request Logs Export + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.requestLogs.exports.download API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.requestLogs.exports.download API Documentation Enterprise + * + * @param int $userId + * @param string $exportId + * @return DownloadFile|null + */ + public function downloadRequestLogsExport(int $userId, string $exportId): ?DownloadFile + { + $path = sprintf('users/%d/ai/request-logs/exports/%s/download', $userId, $exportId); + return $this->_get($path, DownloadFile::class); + } + + /** + * List AI Usage Members + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.usage.members.getMany API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.usage.members.getMany API Documentation Enterprise + * + * @param int $userId + * @param array $params + * string $params[userIds] Comma-separated list of user ids
+ * string $params[orderBy]
+ * integer $params[limit]
+ * integer $params[offset] + * @return ModelCollection + */ + public function listUsageMembers(int $userId, array $params = []): ModelCollection + { + $path = sprintf('users/%d/ai/usage/members', $userId); + return $this->_list($path, AiMemberUsage::class, $params); + } + + /** + * Get AI Usage Member + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.usage.members.get API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.usage.members.get API Documentation Enterprise + * + * @param int $userId + * @param int $memberId + * @return AiMemberUsage|null + */ + public function getUsageMember(int $userId, int $memberId): ?AiMemberUsage + { + $path = sprintf('users/%d/ai/usage/members/%d', $userId, $memberId); + return $this->_get($path, AiMemberUsage::class); + } } diff --git a/src/CrowdinApiClient/Api/Enterprise/AiApi.php b/src/CrowdinApiClient/Api/Enterprise/AiApi.php index 922a9519..b23e2871 100644 --- a/src/CrowdinApiClient/Api/Enterprise/AiApi.php +++ b/src/CrowdinApiClient/Api/Enterprise/AiApi.php @@ -4,6 +4,10 @@ use CrowdinApiClient\Api\AbstractApi; use CrowdinApiClient\Model\AiFileTranslation; +use CrowdinApiClient\Model\AiFineTuningDataset; +use CrowdinApiClient\Model\AiFineTuningEvent; +use CrowdinApiClient\Model\AiFineTuningJob; +use CrowdinApiClient\Model\AiMemberUsage; use CrowdinApiClient\Model\AiPrompt; use CrowdinApiClient\Model\AiPromptCompletion; use CrowdinApiClient\Model\AiProvider; @@ -11,10 +15,13 @@ use CrowdinApiClient\Model\AiProxyChatCompletion; use CrowdinApiClient\Model\AiReport; use CrowdinApiClient\Model\AiRequestLog; +use CrowdinApiClient\Model\AiRequestLogExport; use CrowdinApiClient\Model\AiSettings; use CrowdinApiClient\Model\AiSnippet; +use CrowdinApiClient\Model\AiSupportedModel; use CrowdinApiClient\Model\AiTranslation; use CrowdinApiClient\Model\DownloadFile; +use CrowdinApiClient\Model\ProjectAiSettings; use CrowdinApiClient\ModelCollection; /** @@ -532,4 +539,259 @@ public function deleteSnippet(int $aiSnippetId) $path = sprintf('ai/settings/snippets/%d', $aiSnippetId); return $this->_delete($path); } + + /** + * Get Project AI Settings + * @link https://developer.crowdin.com/api/v2/#operation/api.projects.ai.settings.get API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.ai.settings.get API Documentation Enterprise + * + * @param int $projectId + * @return ProjectAiSettings|null + */ + public function getProjectSettings(int $projectId): ?ProjectAiSettings + { + $path = sprintf('projects/%d/ai/settings', $projectId); + return $this->_get($path, ProjectAiSettings::class); + } + + /** + * Generate AI Prompt Fine-Tuning Dataset + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.prompts.fine-tuning.datasets.post API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.fine-tuning.datasets.post API Documentation Enterprise + * + * @param int $aiPromptId + * @param array $data + * int[] $data[projectIds]
+ * int[] $data[tmIds]
+ * string $data[purpose] Enum: "training" "validation"
+ * string $data[dateFrom]
+ * string $data[dateTo]
+ * integer $data[maxFileSize]
+ * integer $data[minExamplesCount]
+ * integer $data[maxExamplesCount] + * @return AiFineTuningDataset|null + */ + public function generateFineTuningDataset(int $aiPromptId, array $data): ?AiFineTuningDataset + { + $path = sprintf('ai/prompts/%d/fine-tuning/datasets', $aiPromptId); + return $this->_post($path, AiFineTuningDataset::class, $data); + } + + /** + * Get AI Prompt Fine-Tuning Dataset Generation Status + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.prompts.fine-tuning.datasets.get API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.fine-tuning.datasets.get API Documentation Enterprise + * + * @param int $aiPromptId + * @param string $jobIdentifier + * @return AiFineTuningDataset|null + */ + public function getFineTuningDataset(int $aiPromptId, string $jobIdentifier): ?AiFineTuningDataset + { + $path = sprintf('ai/prompts/%d/fine-tuning/datasets/%s', $aiPromptId, $jobIdentifier); + return $this->_get($path, AiFineTuningDataset::class); + } + + /** + * Download AI Prompt Fine-Tuning Dataset + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.prompts.fine-tuning.datasets.download.get API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.fine-tuning.datasets.download.get API Documentation Enterprise + * + * @param int $aiPromptId + * @param string $jobIdentifier + * @return DownloadFile|null + */ + public function downloadFineTuningDataset(int $aiPromptId, string $jobIdentifier): ?DownloadFile + { + $path = sprintf('ai/prompts/%d/fine-tuning/datasets/%s/download', $aiPromptId, $jobIdentifier); + return $this->_get($path, DownloadFile::class); + } + + /** + * List AI Prompt Fine-Tuning Jobs + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.prompts.fine-tuning.jobs.getMany API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.fine-tuning.jobs.getMany API Documentation Enterprise + * + * @param array $params + * string $params[statuses] Comma-separated list of statuses
+ * string $params[orderBy]
+ * integer $params[limit]
+ * integer $params[offset] + * @return ModelCollection + */ + public function listFineTuningJobs(array $params = []): ModelCollection + { + $path = 'ai/prompts/fine-tuning/jobs'; + return $this->_list($path, AiFineTuningJob::class, $params); + } + + /** + * Create AI Prompt Fine-Tuning Job + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.prompts.fine-tuning.jobs.post API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.fine-tuning.jobs.post API Documentation Enterprise + * + * @param int $aiPromptId + * @param array $data + * boolean $data[dryRun]
+ * array $data[hyperparameters] batchSize, learningRateMultiplier, nEpochs
+ * array $data[trainingOptions] required. projectIds, tmIds, dateFrom, dateTo, maxFileSize, minExamplesCount, maxExamplesCount
+ * array $data[validationOptions] same keys as trainingOptions + * @return AiFineTuningJob|null + */ + public function createFineTuningJob(int $aiPromptId, array $data): ?AiFineTuningJob + { + $path = sprintf('ai/prompts/%d/fine-tuning/jobs', $aiPromptId); + return $this->_post($path, AiFineTuningJob::class, $data); + } + + /** + * Get AI Prompt Fine-Tuning Job Status + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.prompts.fine-tuning.jobs.get API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.fine-tuning.jobs.get API Documentation Enterprise + * + * @param int $aiPromptId + * @param string $jobIdentifier + * @return AiFineTuningJob|null + */ + public function getFineTuningJob(int $aiPromptId, string $jobIdentifier): ?AiFineTuningJob + { + $path = sprintf('ai/prompts/%d/fine-tuning/jobs/%s', $aiPromptId, $jobIdentifier); + return $this->_get($path, AiFineTuningJob::class); + } + + /** + * List AI Prompt Fine-Tuning Events + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.prompts.fine-tuning.jobs.events.getMany API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.fine-tuning.jobs.events.getMany API Documentation Enterprise + * + * @param int $aiPromptId + * @param string $jobIdentifier + * @return ModelCollection + */ + public function listFineTuningEvents(int $aiPromptId, string $jobIdentifier): ModelCollection + { + $path = sprintf('ai/prompts/%d/fine-tuning/jobs/%s/events', $aiPromptId, $jobIdentifier); + return $this->_list($path, AiFineTuningEvent::class); + } + + /** + * List AI Provider Models (across all providers) + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.providers.models.crowdin.getMany API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.providers.models.enterprise.getMany API Documentation Enterprise + * + * @return ModelCollection + */ + public function listAllProviderModels(): ModelCollection + { + $path = 'ai/providers/models'; + return $this->_list($path, AiProviderModel::class); + } + + /** + * List Supported AI Provider Models + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.providers.supported-models.crowdin.getMany API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.providers.supported-models.enterprise.getMany API Documentation Enterprise + * + * @param array $params + * string $params[providerType]
+ * boolean $params[enabled]
+ * string $params[orderBy]
+ * integer $params[limit]
+ * integer $params[offset] + * @return ModelCollection + */ + public function listSupportedProviderModels(array $params = []): ModelCollection + { + $path = 'ai/providers/supported-models'; + return $this->_list($path, AiSupportedModel::class, $params); + } + + /** + * Export AI Request Logs + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.requestLogs.exports.post API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.requestLogs.exports.post API Documentation Enterprise + * + * @param array $data + * string $data[format] Enum: "csv"
+ * string $data[requestId]
+ * integer $data[projectId]
+ * integer $data[userId]
+ * integer $data[aiProviderId]
+ * string $data[model]
+ * string $data[sourceAction]
+ * string $data[promptAction]
+ * string[] $data[statuses]
+ * string $data[tokenName]
+ * string $data[oauthClientId]
+ * boolean $data[systemCredentials]
+ * boolean $data[isAutoTriggered]
+ * string $data[createdAfter]
+ * string $data[createdBefore] + * @return AiRequestLogExport|null + */ + public function exportRequestLogs(array $data): ?AiRequestLogExport + { + $path = 'ai/request-logs/exports'; + return $this->_post($path, AiRequestLogExport::class, $data); + } + + /** + * Check AI Request Logs Export Status + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.requestLogs.exports.get API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.requestLogs.exports.get API Documentation Enterprise + * + * @param string $exportId + * @return AiRequestLogExport|null + */ + public function getRequestLogsExport(string $exportId): ?AiRequestLogExport + { + $path = sprintf('ai/request-logs/exports/%s', $exportId); + return $this->_get($path, AiRequestLogExport::class); + } + + /** + * Download AI Request Logs Export + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.requestLogs.exports.download API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.requestLogs.exports.download API Documentation Enterprise + * + * @param string $exportId + * @return DownloadFile|null + */ + public function downloadRequestLogsExport(string $exportId): ?DownloadFile + { + $path = sprintf('ai/request-logs/exports/%s/download', $exportId); + return $this->_get($path, DownloadFile::class); + } + + /** + * List AI Usage Members + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.usage.members.getMany API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.usage.members.getMany API Documentation Enterprise + * + * @param array $params + * string $params[userIds] Comma-separated list of user ids
+ * string $params[orderBy]
+ * integer $params[limit]
+ * integer $params[offset] + * @return ModelCollection + */ + public function listUsageMembers(array $params = []): ModelCollection + { + $path = 'ai/usage/members'; + return $this->_list($path, AiMemberUsage::class, $params); + } + + /** + * Get AI Usage Member + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.usage.members.get API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.usage.members.get API Documentation Enterprise + * + * @param int $memberId + * @return AiMemberUsage|null + */ + public function getUsageMember(int $memberId): ?AiMemberUsage + { + $path = sprintf('ai/usage/members/%d', $memberId); + return $this->_get($path, AiMemberUsage::class); + } } diff --git a/src/CrowdinApiClient/Api/TranslationMemoryApi.php b/src/CrowdinApiClient/Api/TranslationMemoryApi.php index dc03be47..fe3ee5f1 100644 --- a/src/CrowdinApiClient/Api/TranslationMemoryApi.php +++ b/src/CrowdinApiClient/Api/TranslationMemoryApi.php @@ -206,62 +206,6 @@ public function updateSegment(int $tmId, int $segmentId, array $data): ?Translat return $this->_patch($path, TranslationMemorySegment::class, $data); } - /** - * Create TM Segment Records - * @link https://developer.crowdin.com/api/v2/#operation/api.tms.segments.records.post API Documentation - * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.tms.segments.records.post API Documentation Enterprise - * - * @param int $tmId - * @param int $segmentId - * @param array $data - * array $data[records] required
- * string $data[records][][languageId]
- * string $data[records][][text] - * @return TranslationMemorySegment|null - */ - public function createSegmentRecords(int $tmId, int $segmentId, array $data): ?TranslationMemorySegment - { - $path = sprintf('tms/%d/segments/%d/records', $tmId, $segmentId); - return $this->_create($path, TranslationMemorySegment::class, $data); - } - - /** - * Edit TM Segment Record - * @link https://developer.crowdin.com/api/v2/#operation/api.tms.segments.records.patch API Documentation - * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.tms.segments.records.patch API Documentation Enterprise - * - * @param int $tmId - * @param int $segmentId - * @param int $recordId - * @param array $data JSON Patch array. Path: /text (replace) - * @return TranslationMemorySegment|null - */ - public function updateSegmentRecord( - int $tmId, - int $segmentId, - int $recordId, - array $data - ): ?TranslationMemorySegment { - $path = sprintf('tms/%d/segments/%d/records/%d', $tmId, $segmentId, $recordId); - return $this->_patch($path, TranslationMemorySegment::class, $data); - } - - /** - * Delete TM Segment Record - * @link https://developer.crowdin.com/api/v2/#operation/api.tms.segments.records.delete API Documentation - * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.tms.segments.records.delete API Documentation Enterprise - * - * @param int $tmId - * @param int $segmentId - * @param int $recordId - * @return mixed - */ - public function deleteSegmentRecord(int $tmId, int $segmentId, int $recordId) - { - $path = sprintf('tms/%d/segments/%d/records/%d', $tmId, $segmentId, $recordId); - return $this->_delete($path); - } - /** * Export TM * @link https://developer.crowdin.com/api/v2/#operation/api.tms.exports.post API Documentation diff --git a/src/CrowdinApiClient/Model/AiFineTuningDataset.php b/src/CrowdinApiClient/Model/AiFineTuningDataset.php new file mode 100644 index 00000000..dafd2224 --- /dev/null +++ b/src/CrowdinApiClient/Model/AiFineTuningDataset.php @@ -0,0 +1,9 @@ +id = (string)$this->getDataProperty('id'); + $this->type = (string)$this->getDataProperty('type'); + $this->message = (string)$this->getDataProperty('message'); + $this->eventData = (array)$this->getDataProperty('data'); + $this->createdAt = (string)$this->getDataProperty('createdAt'); + } + + public function getId(): string + { + return $this->id; + } + + /** + * @return string message or metrics + */ + public function getType(): string + { + return $this->type; + } + + public function getMessage(): string + { + return $this->message; + } + + /** + * @return array step, totalSteps, trainingLoss, validationLoss, fullValidationLoss + */ + public function getEventData(): array + { + return $this->eventData; + } + + public function getCreatedAt(): string + { + return $this->createdAt; + } +} diff --git a/src/CrowdinApiClient/Model/AiFineTuningJob.php b/src/CrowdinApiClient/Model/AiFineTuningJob.php new file mode 100644 index 00000000..8a72d28d --- /dev/null +++ b/src/CrowdinApiClient/Model/AiFineTuningJob.php @@ -0,0 +1,9 @@ +user = (array)$this->getDataProperty('user'); + $this->dailyCostLimit = $this->nullableFloat('dailyCostLimit'); + $this->dailyCostSpent = (float)$this->getDataProperty('dailyCostSpent'); + $this->dailyResetAt = (string)$this->getDataProperty('dailyResetAt'); + $this->monthlyCostLimit = $this->nullableFloat('monthlyCostLimit'); + $this->monthlyCostSpent = (float)$this->getDataProperty('monthlyCostSpent'); + $this->monthlyResetAt = (string)$this->getDataProperty('monthlyResetAt'); + } + + /** + * @return array id, username, fullName, avatarUrl + */ + public function getUser(): array + { + return $this->user; + } + + public function getDailyCostLimit(): ?float + { + return $this->dailyCostLimit; + } + + public function getDailyCostSpent(): float + { + return $this->dailyCostSpent; + } + + public function getDailyResetAt(): string + { + return $this->dailyResetAt; + } + + public function getMonthlyCostLimit(): ?float + { + return $this->monthlyCostLimit; + } + + public function getMonthlyCostSpent(): float + { + return $this->monthlyCostSpent; + } + + public function getMonthlyResetAt(): string + { + return $this->monthlyResetAt; + } +} diff --git a/src/CrowdinApiClient/Model/AiProviderModel.php b/src/CrowdinApiClient/Model/AiProviderModel.php index e7365b98..40af5d67 100644 --- a/src/CrowdinApiClient/Model/AiProviderModel.php +++ b/src/CrowdinApiClient/Model/AiProviderModel.php @@ -18,6 +18,30 @@ class AiProviderModel extends BaseModel /** @var int|null */ protected $providerId; + /** @var int|null */ + protected $contextWindow; + + /** @var int|null */ + protected $maxOutputTokens; + + /** @var bool|null */ + protected $supportsStreaming; + + /** @var bool|null */ + protected $supportsFunctionCalling; + + /** @var bool|null */ + protected $supportsJsonMode; + + /** @var bool|null */ + protected $supportsJsonSchema; + + /** @var bool|null */ + protected $supportsVision; + + /** @var bool */ + protected $isCompatibleWithAiLimit; + public function __construct(array $data = []) { parent::__construct($data); @@ -26,6 +50,14 @@ public function __construct(array $data = []) $this->provider = $this->getDataProperty('provider'); $this->providerName = $this->getDataProperty('providerName'); $this->providerId = $this->getDataProperty('providerId'); + $this->contextWindow = $this->nullableInt('contextWindow'); + $this->maxOutputTokens = $this->nullableInt('maxOutputTokens'); + $this->supportsStreaming = $this->nullableBool('supportsStreaming'); + $this->supportsFunctionCalling = $this->nullableBool('supportsFunctionCalling'); + $this->supportsJsonMode = $this->nullableBool('supportsJsonMode'); + $this->supportsJsonSchema = $this->nullableBool('supportsJsonSchema'); + $this->supportsVision = $this->nullableBool('supportsVision'); + $this->isCompatibleWithAiLimit = (bool)$this->getDataProperty('isCompatibleWithAiLimit'); } public function getId(): string @@ -47,4 +79,44 @@ public function getProviderId(): ?int { return $this->providerId; } + + public function getContextWindow(): ?int + { + return $this->contextWindow; + } + + public function getMaxOutputTokens(): ?int + { + return $this->maxOutputTokens; + } + + public function getSupportsStreaming(): ?bool + { + return $this->supportsStreaming; + } + + public function getSupportsFunctionCalling(): ?bool + { + return $this->supportsFunctionCalling; + } + + public function getSupportsJsonMode(): ?bool + { + return $this->supportsJsonMode; + } + + public function getSupportsJsonSchema(): ?bool + { + return $this->supportsJsonSchema; + } + + public function getSupportsVision(): ?bool + { + return $this->supportsVision; + } + + public function getIsCompatibleWithAiLimit(): bool + { + return $this->isCompatibleWithAiLimit; + } } diff --git a/src/CrowdinApiClient/Model/AiRequestLogExport.php b/src/CrowdinApiClient/Model/AiRequestLogExport.php new file mode 100644 index 00000000..eddbaf03 --- /dev/null +++ b/src/CrowdinApiClient/Model/AiRequestLogExport.php @@ -0,0 +1,23 @@ +eta = $this->nullableString('eta'); + } + + public function getEta(): ?string + { + return $this->eta; + } +} diff --git a/src/CrowdinApiClient/Model/AiSupportedModel.php b/src/CrowdinApiClient/Model/AiSupportedModel.php new file mode 100644 index 00000000..6ff5185f --- /dev/null +++ b/src/CrowdinApiClient/Model/AiSupportedModel.php @@ -0,0 +1,158 @@ +providerId = $this->nullableInt('providerId'); + $this->providerType = (string)$this->getDataProperty('providerType'); + $this->providerName = (string)$this->getDataProperty('providerName'); + $this->id = (string)$this->getDataProperty('id'); + $this->displayName = (string)$this->getDataProperty('displayName'); + $this->supportReasoning = (bool)$this->getDataProperty('supportReasoning'); + $this->intelligence = (int)$this->getDataProperty('intelligence'); + $this->speed = (int)$this->getDataProperty('speed'); + $this->price = (array)$this->getDataProperty('price'); + $this->modalities = (array)$this->getDataProperty('modalities'); + $this->contextWindow = (int)$this->getDataProperty('contextWindow'); + $this->maxOutputTokens = (int)$this->getDataProperty('maxOutputTokens'); + $this->knowledgeCutoff = $this->nullableString('knowledgeCutoff'); + $this->releaseDate = $this->nullableString('releaseDate'); + $this->features = (array)$this->getDataProperty('features'); + } + + public function getProviderId(): ?int + { + return $this->providerId; + } + + public function getProviderType(): string + { + return $this->providerType; + } + + public function getProviderName(): string + { + return $this->providerName; + } + + public function getId(): string + { + return $this->id; + } + + public function getDisplayName(): string + { + return $this->displayName; + } + + public function getSupportReasoning(): bool + { + return $this->supportReasoning; + } + + public function getIntelligence(): int + { + return $this->intelligence; + } + + public function getSpeed(): int + { + return $this->speed; + } + + /** + * @return array input, output + */ + public function getPrice(): array + { + return $this->price; + } + + /** + * @return array input, output + */ + public function getModalities(): array + { + return $this->modalities; + } + + public function getContextWindow(): int + { + return $this->contextWindow; + } + + public function getMaxOutputTokens(): int + { + return $this->maxOutputTokens; + } + + public function getKnowledgeCutoff(): ?string + { + return $this->knowledgeCutoff; + } + + public function getReleaseDate(): ?string + { + return $this->releaseDate; + } + + /** + * @return array streaming, structuredOutput, functionCalling + */ + public function getFeatures(): array + { + return $this->features; + } +} diff --git a/src/CrowdinApiClient/Model/BaseModel.php b/src/CrowdinApiClient/Model/BaseModel.php index b34d217b..a5acc3d9 100644 --- a/src/CrowdinApiClient/Model/BaseModel.php +++ b/src/CrowdinApiClient/Model/BaseModel.php @@ -35,4 +35,36 @@ public function getProperties(): array { return get_object_vars($this); } + + /** + * Returns the property cast to bool, or null when it is missing or null. + */ + protected function nullableBool(string $property): ?bool + { + return $this->getDataProperty($property) !== null ? (bool)$this->getDataProperty($property) : null; + } + + /** + * Returns the property cast to int, or null when it is missing or null. + */ + protected function nullableInt(string $property): ?int + { + return $this->getDataProperty($property) !== null ? (int)$this->getDataProperty($property) : null; + } + + /** + * Returns the property cast to float, or null when it is missing or null. + */ + protected function nullableFloat(string $property): ?float + { + return $this->getDataProperty($property) !== null ? (float)$this->getDataProperty($property) : null; + } + + /** + * Returns the property cast to string, or null when it is missing or null. + */ + protected function nullableString(string $property): ?string + { + return $this->getDataProperty($property) !== null ? (string)$this->getDataProperty($property) : null; + } } diff --git a/src/CrowdinApiClient/Model/Project.php b/src/CrowdinApiClient/Model/Project.php index d8fa353d..740798c3 100644 --- a/src/CrowdinApiClient/Model/Project.php +++ b/src/CrowdinApiClient/Model/Project.php @@ -309,9 +309,7 @@ public function __construct(array $data = []) $this->customQaCheckIds = (array)$this->getDataProperty('customQaCheckIds'); $this->languageMapping = (array)$this->getDataProperty('languageMapping'); $this->glossaryAccess = (bool)$this->getDataProperty('glossaryAccess'); - $this->glossaryAccessOption = $this->getDataProperty('glossaryAccessOption') !== null - ? (string)$this->getDataProperty('glossaryAccessOption') - : null; + $this->glossaryAccessOption = $this->nullableString('glossaryAccessOption'); $this->isSuspended = (bool)$this->getDataProperty('isSuspended'); $this->normalizePlaceholder = (bool)$this->getDataProperty('normalizePlaceholder'); $this->saveMetaInfoInSource = (bool)$this->getDataProperty('saveMetaInfoInSource'); diff --git a/src/CrowdinApiClient/Model/ProjectAiSettings.php b/src/CrowdinApiClient/Model/ProjectAiSettings.php new file mode 100644 index 00000000..b108129b --- /dev/null +++ b/src/CrowdinApiClient/Model/ProjectAiSettings.php @@ -0,0 +1,53 @@ +editorSuggestionAiPromptId = $this->nullableInt('editorSuggestionAiPromptId'); + $this->alignmentActionAiPromptId = $this->nullableInt('alignmentActionAiPromptId'); + $this->qaCheckActionAiPromptId = $this->nullableInt('qaCheckActionAiPromptId'); + $this->contextReviewAiPromptId = $this->nullableInt('contextReviewAiPromptId'); + } + + public function getEditorSuggestionAiPromptId(): ?int + { + return $this->editorSuggestionAiPromptId; + } + + /** + * Crowdin Enterprise only + */ + public function getAlignmentActionAiPromptId(): ?int + { + return $this->alignmentActionAiPromptId; + } + + public function getQaCheckActionAiPromptId(): ?int + { + return $this->qaCheckActionAiPromptId; + } + + public function getContextReviewAiPromptId(): ?int + { + return $this->contextReviewAiPromptId; + } +} diff --git a/src/CrowdinApiClient/Model/QaCheck.php b/src/CrowdinApiClient/Model/QaCheck.php index ae62c095..59827a09 100644 --- a/src/CrowdinApiClient/Model/QaCheck.php +++ b/src/CrowdinApiClient/Model/QaCheck.php @@ -68,12 +68,8 @@ public function __construct(array $data = []) $this->validationDescription = (string)$this->getDataProperty('validationDescription'); $this->pluralId = (int)$this->getDataProperty('pluralId'); $this->text = (string)$this->getDataProperty('text'); - $this->pluralCategoryName = $this->getDataProperty('pluralCategoryName') !== null - ? (string)$this->getDataProperty('pluralCategoryName') - : null; - $this->translation = $this->getDataProperty('translation') !== null - ? (string)$this->getDataProperty('translation') - : null; + $this->pluralCategoryName = $this->nullableString('pluralCategoryName'); + $this->translation = $this->nullableString('translation'); } /** diff --git a/src/CrowdinApiClient/Model/ReportSettingsTemplate.php b/src/CrowdinApiClient/Model/ReportSettingsTemplate.php index 4267999c..52cee111 100644 --- a/src/CrowdinApiClient/Model/ReportSettingsTemplate.php +++ b/src/CrowdinApiClient/Model/ReportSettingsTemplate.php @@ -71,12 +71,8 @@ public function __construct(array $data = []) parent::__construct($data); $this->id = (int)$this->getDataProperty('id'); - $this->projectId = $this->getDataProperty('projectId') !== null - ? (int)$this->getDataProperty('projectId') - : null; - $this->groupId = $this->getDataProperty('groupId') !== null - ? (int)$this->getDataProperty('groupId') - : null; + $this->projectId = $this->nullableInt('projectId'); + $this->groupId = $this->nullableInt('groupId'); $this->name = (string)$this->getDataProperty('name'); $this->currency = (string)$this->getDataProperty('currency'); $this->unit = (string)$this->getDataProperty('unit'); diff --git a/src/CrowdinApiClient/Model/Screenshot.php b/src/CrowdinApiClient/Model/Screenshot.php index d80b4111..de52df4b 100644 --- a/src/CrowdinApiClient/Model/Screenshot.php +++ b/src/CrowdinApiClient/Model/Screenshot.php @@ -68,7 +68,7 @@ public function __construct(array $data = []) $this->id = (int)$this->getDataProperty('id'); $this->userId = (int)$this->getDataProperty('userId'); $this->url = (string)$this->getDataProperty('url'); - $this->webUrl = $this->getDataProperty('webUrl') !== null ? (string)$this->getDataProperty('webUrl') : null; + $this->webUrl = $this->nullableString('webUrl'); $this->name = (string)$this->getDataProperty('name'); $this->size = (array)$this->getDataProperty('size'); $this->tagsCount = (int)$this->getDataProperty('tagsCount'); diff --git a/tests/CrowdinApiClient/Api/AiApiTest.php b/tests/CrowdinApiClient/Api/AiApiTest.php index 22b23299..f4d63560 100644 --- a/tests/CrowdinApiClient/Api/AiApiTest.php +++ b/tests/CrowdinApiClient/Api/AiApiTest.php @@ -3,6 +3,10 @@ namespace CrowdinApiClient\Tests\Api; use CrowdinApiClient\Model\AiFileTranslation; +use CrowdinApiClient\Model\AiFineTuningDataset; +use CrowdinApiClient\Model\AiFineTuningEvent; +use CrowdinApiClient\Model\AiFineTuningJob; +use CrowdinApiClient\Model\AiMemberUsage; use CrowdinApiClient\Model\AiPrompt; use CrowdinApiClient\Model\AiPromptCompletion; use CrowdinApiClient\Model\AiProvider; @@ -10,10 +14,13 @@ use CrowdinApiClient\Model\AiProxyChatCompletion; use CrowdinApiClient\Model\AiReport; use CrowdinApiClient\Model\AiRequestLog; +use CrowdinApiClient\Model\AiRequestLogExport; use CrowdinApiClient\Model\AiSettings; use CrowdinApiClient\Model\AiSnippet; +use CrowdinApiClient\Model\AiSupportedModel; use CrowdinApiClient\Model\AiTranslation; use CrowdinApiClient\Model\DownloadFile; +use CrowdinApiClient\Model\ProjectAiSettings; use CrowdinApiClient\ModelCollection; class AiApiTest extends AbstractTestApi @@ -30,25 +37,48 @@ public function testTranslateStrings(): void 'path' => '/users/1/ai/translate', 'method' => 'post', 'body' => json_encode($params), - 'response' => '{ - "data": { - "sourceLanguageId": "en", - "targetLanguageId": "uk", - "translations": [ - "Перекладений текст 1", - "Перекладений текст 2" - ] - } - }', + 'response' => json_encode([ + 'data' => [ + 'sourceLanguageId' => 'en', + 'targetLanguageId' => 'uk', + 'translations' => ['Перекладений текст 1', 'Перекладений текст 2'], + ], + ]), ]); $aiTranslation = $this->crowdin->ai->translateStrings(1, $params); + $this->assertInstanceOf(AiTranslation::class, $aiTranslation); $this->assertEquals('en', $aiTranslation->getSourceLanguageId()); $this->assertEquals('uk', $aiTranslation->getTargetLanguageId()); $this->assertEquals(['Перекладений текст 1', 'Перекладений текст 2'], $aiTranslation->getTranslations()); } + private function fileTranslationResponse(): string + { + return json_encode([ + 'data' => [ + 'identifier' => '50fb3506-4127-4ba8-8296-f97dc7e3e0c3', + 'status' => 'finished', + 'progress' => 100, + 'attributes' => [ + 'stage' => 'translate', + 'error' => null, + 'downloadName' => 'file.pdf', + 'sourceLanguageId' => 'en', + 'targetLanguageId' => 'uk', + 'originalFileName' => 'Sample_Chrome.json', + 'detectedType' => 'chrome', + 'parserVersion' => 2, + ], + 'createdAt' => '2026-01-23T11:26:54+00:00', + 'updatedAt' => '2026-01-23T11:26:54+00:00', + 'startedAt' => '2026-01-23T11:26:54+00:00', + 'finishedAt' => '2026-01-23T11:26:54+00:00', + ], + ]); + } + public function testCreateFileTranslation(): void { $params = [ @@ -69,30 +99,11 @@ public function testCreateFileTranslation(): void 'path' => '/users/1/ai/file-translations', 'method' => 'post', 'body' => json_encode($params), - 'response' => '{ - "data": { - "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", - "status": "finished", - "progress": 100, - "attributes": { - "stage": "translate", - "error": null, - "downloadName": "file.pdf", - "sourceLanguageId": "en", - "targetLanguageId": "uk", - "originalFileName": "Sample_Chrome.json", - "detectedType": "chrome", - "parserVersion": 2 - }, - "createdAt": "2026-01-23T11:26:54+00:00", - "updatedAt": "2026-01-23T11:26:54+00:00", - "startedAt": "2026-01-23T11:26:54+00:00", - "finishedAt": "2026-01-23T11:26:54+00:00" - } - }', + 'response' => $this->fileTranslationResponse(), ]); $fileTranslation = $this->crowdin->ai->createFileTranslation(1, $params); + $this->assertInstanceOf(AiFileTranslation::class, $fileTranslation); $this->assertEquals('50fb3506-4127-4ba8-8296-f97dc7e3e0c3', $fileTranslation->getIdentifier()); $this->assertEquals('finished', $fileTranslation->getStatus()); @@ -105,30 +116,11 @@ public function testGetFileTranslation(): void $this->mockRequest([ 'path' => '/users/1/ai/file-translations/50fb3506-4127-4ba8-8296-f97dc7e3e0c3', 'method' => 'get', - 'response' => '{ - "data": { - "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", - "status": "finished", - "progress": 100, - "attributes": { - "stage": "translate", - "error": null, - "downloadName": "file.pdf", - "sourceLanguageId": "en", - "targetLanguageId": "uk", - "originalFileName": "Sample_Chrome.json", - "detectedType": "chrome", - "parserVersion": 2 - }, - "createdAt": "2026-01-23T11:26:54+00:00", - "updatedAt": "2026-01-23T11:26:54+00:00", - "startedAt": "2026-01-23T11:26:54+00:00", - "finishedAt": "2026-01-23T11:26:54+00:00" - } - }', + 'response' => $this->fileTranslationResponse(), ]); $fileTranslation = $this->crowdin->ai->getFileTranslation(1, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + $this->assertInstanceOf(AiFileTranslation::class, $fileTranslation); $this->assertEquals('50fb3506-4127-4ba8-8296-f97dc7e3e0c3', $fileTranslation->getIdentifier()); $this->assertEquals('finished', $fileTranslation->getStatus()); @@ -150,20 +142,26 @@ public function testDeleteFileTranslation(): void $this->crowdin->ai->deleteFileTranslation(1, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); } + private function downloadFileResponse(): string + { + return json_encode([ + 'data' => [ + 'url' => 'https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff', + 'expireIn' => '2019-09-20T10:31:21+00:00', + ], + ]); + } + public function testDownloadFileTranslation(): void { $this->mockRequest([ 'path' => '/users/1/ai/file-translations/50fb3506-4127-4ba8-8296-f97dc7e3e0c3/download', 'method' => 'get', - 'response' => '{ - "data": { - "url": "https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff", - "expireIn": "2019-09-20T10:31:21+00:00" - } - }', + 'response' => $this->downloadFileResponse(), ]); $downloadFile = $this->crowdin->ai->downloadFileTranslation(1, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + $this->assertInstanceOf(DownloadFile::class, $downloadFile); $this->assertEquals('https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff', $downloadFile->getUrl()); $this->assertEquals('2019-09-20T10:31:21+00:00', $downloadFile->getExpireIn()); @@ -174,15 +172,11 @@ public function testDownloadFileTranslationStrings(): void $this->mockRequest([ 'path' => '/users/1/ai/file-translations/50fb3506-4127-4ba8-8296-f97dc7e3e0c3/translations', 'method' => 'get', - 'response' => '{ - "data": { - "url": "https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff", - "expireIn": "2019-09-20T10:31:21+00:00" - } - }', + 'response' => $this->downloadFileResponse(), ]); $downloadFile = $this->crowdin->ai->downloadFileTranslationStrings(1, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + $this->assertInstanceOf(DownloadFile::class, $downloadFile); $this->assertEquals('https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff', $downloadFile->getUrl()); $this->assertEquals('2019-09-20T10:31:21+00:00', $downloadFile->getExpireIn()); @@ -193,35 +187,39 @@ public function testListPrompts(): void $this->mockRequest([ 'path' => '/users/1/ai/prompts', 'method' => 'get', - 'response' => '{ - "data": [ - { - "data": { - "id": 2, - "name": "Pre-translate prompt", - "action": "pre_translate", - "aiProviderId": 2, - "aiModelId": "gpt-5.4", - "isEnabled": true, - "enabledProjectIds": [1], - "config": {}, - "promptPreview": null, - "isFineTuningAvailable": true, - "createdBy": 123, - "updatedBy": 456, - "lastUsedBy": 789, - "lastUsedAt": "2019-09-25T14:30:00+00:00", - "usageCount": 42, - "createdAt": "2019-09-20T11:11:05+00:00", - "updatedAt": "2019-09-20T12:22:20+00:00" - } - } - ], - "pagination": {"offset": 0, "limit": 25} - }', + 'response' => json_encode([ + 'data' => [ + [ + 'data' => [ + 'id' => 2, + 'name' => 'Pre-translate prompt', + 'action' => 'pre_translate', + 'aiProviderId' => 2, + 'aiModelId' => 'gpt-5.4', + 'isEnabled' => true, + 'enabledProjectIds' => [1], + 'config' => [], + 'promptPreview' => null, + 'isFineTuningAvailable' => true, + 'createdBy' => 123, + 'updatedBy' => 456, + 'lastUsedBy' => 789, + 'lastUsedAt' => '2019-09-25T14:30:00+00:00', + 'usageCount' => 42, + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + ], + ], + ], + 'pagination' => [ + 'offset' => 0, + 'limit' => 25, + ], + ]), ]); $prompts = $this->crowdin->ai->listPrompts(1); + $this->assertInstanceOf(ModelCollection::class, $prompts); $this->assertInstanceOf(AiPrompt::class, $prompts[0]); $this->assertEquals(2, $prompts[0]->getId()); @@ -241,30 +239,33 @@ public function testCreatePrompt(): void 'path' => '/users/1/ai/prompts', 'method' => 'post', 'body' => json_encode($params), - 'response' => '{ - "data": { - "id": 2, - "name": "Pre-translate prompt", - "action": "pre_translate", - "aiProviderId": 2, - "aiModelId": "gpt-5.4", - "isEnabled": true, - "enabledProjectIds": [1], - "config": {"mode": "basic"}, - "promptPreview": null, - "isFineTuningAvailable": true, - "createdBy": 123, - "updatedBy": 456, - "lastUsedBy": 789, - "lastUsedAt": "2019-09-25T14:30:00+00:00", - "usageCount": 42, - "createdAt": "2019-09-20T11:11:05+00:00", - "updatedAt": "2019-09-20T12:22:20+00:00" - } - }', + 'response' => json_encode([ + 'data' => [ + 'id' => 2, + 'name' => 'Pre-translate prompt', + 'action' => 'pre_translate', + 'aiProviderId' => 2, + 'aiModelId' => 'gpt-5.4', + 'isEnabled' => true, + 'enabledProjectIds' => [1], + 'config' => [ + 'mode' => 'basic', + ], + 'promptPreview' => null, + 'isFineTuningAvailable' => true, + 'createdBy' => 123, + 'updatedBy' => 456, + 'lastUsedBy' => 789, + 'lastUsedAt' => '2019-09-25T14:30:00+00:00', + 'usageCount' => 42, + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + ], + ]), ]); $prompt = $this->crowdin->ai->createPrompt(1, $params); + $this->assertInstanceOf(AiPrompt::class, $prompt); $this->assertEquals(2, $prompt->getId()); $this->assertEquals('Pre-translate prompt', $prompt->getName()); @@ -276,45 +277,46 @@ public function testGetPrompt(): void $this->mockRequest([ 'path' => '/users/1/ai/prompts/2', 'method' => 'get', - 'response' => '{ - "data": { - "id": 2, - "name": "Pre-translate prompt", - "action": "pre_translate", - "aiProviderId": 2, - "aiModelId": "gpt-5.4", - "isEnabled": true, - "enabledProjectIds": [1], - "config": { - "mode": "basic", - "snippets": ["%custom:companyDescription%"], - "otherLanguageTranslations": { - "isEnabled": true, - "languageIds": ["uk"] - }, - "glossaryTerms": true, - "tmSuggestions": true, - "fileContext": true, - "generateFileSummary": true, - "screenshots": true, - "projectContext": true, - "siblingsStrings": true, - "retryOnQaIssues": true - }, - "promptPreview": null, - "isFineTuningAvailable": true, - "createdBy": 123, - "updatedBy": 456, - "lastUsedBy": 789, - "lastUsedAt": "2019-09-25T14:30:00+00:00", - "usageCount": 42, - "createdAt": "2019-09-20T11:11:05+00:00", - "updatedAt": "2019-09-20T12:22:20+00:00" - } - }', + 'response' => json_encode([ + 'data' => [ + 'id' => 2, + 'name' => 'Pre-translate prompt', + 'action' => 'pre_translate', + 'aiProviderId' => 2, + 'aiModelId' => 'gpt-5.4', + 'isEnabled' => true, + 'enabledProjectIds' => [1], + 'config' => [ + 'mode' => 'basic', + 'snippets' => ['%custom:companyDescription%'], + 'otherLanguageTranslations' => [ + 'isEnabled' => true, + 'languageIds' => ['uk'], + ], + 'glossaryTerms' => true, + 'tmSuggestions' => true, + 'fileContext' => true, + 'generateFileSummary' => true, + 'screenshots' => true, + 'projectContext' => true, + 'siblingsStrings' => true, + 'retryOnQaIssues' => true, + ], + 'promptPreview' => null, + 'isFineTuningAvailable' => true, + 'createdBy' => 123, + 'updatedBy' => 456, + 'lastUsedBy' => 789, + 'lastUsedAt' => '2019-09-25T14:30:00+00:00', + 'usageCount' => 42, + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + ], + ]), ]); $prompt = $this->crowdin->ai->getPrompt(1, 2); + $this->assertInstanceOf(AiPrompt::class, $prompt); $this->assertEquals(2, $prompt->getId()); $this->assertEquals(2, $prompt->getAiProviderId()); @@ -353,30 +355,31 @@ public function testClonePrompt(): void 'path' => '/users/1/ai/prompts/2/clones', 'method' => 'post', 'body' => json_encode($params), - 'response' => '{ - "data": { - "id": 2, - "name": "Pre-translate prompt", - "action": "pre_translate", - "aiProviderId": 2, - "aiModelId": "gpt-5.4", - "isEnabled": true, - "enabledProjectIds": [1], - "config": {}, - "promptPreview": null, - "isFineTuningAvailable": true, - "createdBy": 123, - "updatedBy": 456, - "lastUsedBy": 789, - "lastUsedAt": "2019-09-25T14:30:00+00:00", - "usageCount": 42, - "createdAt": "2019-09-20T11:11:05+00:00", - "updatedAt": "2019-09-20T12:22:20+00:00" - } - }', + 'response' => json_encode([ + 'data' => [ + 'id' => 2, + 'name' => 'Pre-translate prompt', + 'action' => 'pre_translate', + 'aiProviderId' => 2, + 'aiModelId' => 'gpt-5.4', + 'isEnabled' => true, + 'enabledProjectIds' => [1], + 'config' => [], + 'promptPreview' => null, + 'isFineTuningAvailable' => true, + 'createdBy' => 123, + 'updatedBy' => 456, + 'lastUsedBy' => 789, + 'lastUsedAt' => '2019-09-25T14:30:00+00:00', + 'usageCount' => 42, + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + ], + ]), ]); $cloned = $this->crowdin->ai->clonePrompt(1, 2, $params); + $this->assertInstanceOf(AiPrompt::class, $cloned); $this->assertEquals(2, $cloned->getId()); $this->assertEquals('Pre-translate prompt', $cloned->getName()); @@ -396,23 +399,11 @@ public function testCreatePromptCompletion(): void 'path' => '/users/1/ai/prompts/2/completions', 'method' => 'post', 'body' => json_encode($params), - 'response' => '{ - "data": { - "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", - "status": "finished", - "progress": 100, - "attributes": { - "aiPromptId": 38 - }, - "createdAt": "2019-09-23T11:26:54+00:00", - "updatedAt": "2019-09-23T11:26:54+00:00", - "startedAt": "2019-09-23T11:26:54+00:00", - "finishedAt": "2019-09-23T11:26:54+00:00" - } - }', + 'response' => $this->promptCompletionResponse(), ]); $completion = $this->crowdin->ai->createPromptCompletion(1, 2, $params); + $this->assertInstanceOf(AiPromptCompletion::class, $completion); $this->assertEquals('50fb3506-4127-4ba8-8296-f97dc7e3e0c3', $completion->getIdentifier()); $this->assertEquals('finished', $completion->getStatus()); @@ -424,23 +415,11 @@ public function testGetPromptCompletion(): void $this->mockRequest([ 'path' => '/users/1/ai/prompts/2/completions/50fb3506-4127-4ba8-8296-f97dc7e3e0c3', 'method' => 'get', - 'response' => '{ - "data": { - "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", - "status": "finished", - "progress": 100, - "attributes": { - "aiPromptId": 38 - }, - "createdAt": "2019-09-23T11:26:54+00:00", - "updatedAt": "2019-09-23T11:26:54+00:00", - "startedAt": "2019-09-23T11:26:54+00:00", - "finishedAt": "2019-09-23T11:26:54+00:00" - } - }', + 'response' => $this->promptCompletionResponse(), ]); $completion = $this->crowdin->ai->getPromptCompletion(1, 2, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + $this->assertInstanceOf(AiPromptCompletion::class, $completion); $this->assertEquals('finished', $completion->getStatus()); $this->assertEquals(100, $completion->getProgress()); @@ -450,6 +429,24 @@ public function testGetPromptCompletion(): void $this->assertEquals('2019-09-23T11:26:54+00:00', $completion->getFinishedAt()); } + private function promptCompletionResponse(): string + { + return json_encode([ + 'data' => [ + 'identifier' => '50fb3506-4127-4ba8-8296-f97dc7e3e0c3', + 'status' => 'finished', + 'progress' => 100, + 'attributes' => [ + 'aiPromptId' => 38, + ], + 'createdAt' => '2019-09-23T11:26:54+00:00', + 'updatedAt' => '2019-09-23T11:26:54+00:00', + 'startedAt' => '2019-09-23T11:26:54+00:00', + 'finishedAt' => '2019-09-23T11:26:54+00:00', + ], + ]); + } + public function testDeletePromptCompletion(): void { $this->mockRequest([ @@ -466,15 +463,11 @@ public function testDownloadPromptCompletion(): void $this->mockRequest([ 'path' => '/users/1/ai/prompts/2/completions/50fb3506-4127-4ba8-8296-f97dc7e3e0c3/download', 'method' => 'get', - 'response' => '{ - "data": { - "url": "https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff", - "expireIn": "2019-09-20T10:31:21+00:00" - } - }', + 'response' => $this->downloadFileResponse(), ]); $download = $this->crowdin->ai->downloadPromptCompletion(1, 2, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + $this->assertInstanceOf(DownloadFile::class, $download); $this->assertEquals('https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff', $download->getUrl()); $this->assertEquals('2019-09-20T10:31:21+00:00', $download->getExpireIn()); @@ -485,35 +478,41 @@ public function testListProviders(): void $this->mockRequest([ 'path' => '/users/1/ai/providers', 'method' => 'get', - 'response' => '{ - "data": [ - { - "data": { - "id": 2, - "name": "OpenAI", - "type": "open_ai", - "credentials": {"apiKey": "sk-..."}, - "config": { - "actionRules": [ - { - "action": "pre_translate", - "availableAiModelIds": ["gpt-5.4"] - } - ] - }, - "isEnabled": true, - "useSystemCredentials": false, - "createdAt": "2019-09-20T11:11:05+00:00", - "updatedAt": "2019-09-20T12:22:20+00:00", - "promptsCount": 42 - } - } - ], - "pagination": {"offset": 0, "limit": 25} - }', + 'response' => json_encode([ + 'data' => [ + [ + 'data' => [ + 'id' => 2, + 'name' => 'OpenAI', + 'type' => 'open_ai', + 'credentials' => [ + 'apiKey' => 'sk-...', + ], + 'config' => [ + 'actionRules' => [ + [ + 'action' => 'pre_translate', + 'availableAiModelIds' => ['gpt-5.4'], + ], + ], + ], + 'isEnabled' => true, + 'useSystemCredentials' => false, + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + 'promptsCount' => 42, + ], + ], + ], + 'pagination' => [ + 'offset' => 0, + 'limit' => 25, + ], + ]), ]); $providers = $this->crowdin->ai->listProviders(1); + $this->assertInstanceOf(ModelCollection::class, $providers); $this->assertInstanceOf(AiProvider::class, $providers[0]); $this->assertEquals(2, $providers[0]->getId()); @@ -535,23 +534,26 @@ public function testCreateProvider(): void 'path' => '/users/1/ai/providers', 'method' => 'post', 'body' => json_encode($params), - 'response' => '{ - "data": { - "id": 2, - "name": "OpenAI", - "type": "open_ai", - "credentials": {"apiKey": "sk-..."}, - "config": {}, - "isEnabled": true, - "useSystemCredentials": false, - "createdAt": "2019-09-20T11:11:05+00:00", - "updatedAt": "2019-09-20T12:22:20+00:00", - "promptsCount": 42 - } - }', + 'response' => json_encode([ + 'data' => [ + 'id' => 2, + 'name' => 'OpenAI', + 'type' => 'open_ai', + 'credentials' => [ + 'apiKey' => 'sk-...', + ], + 'config' => [], + 'isEnabled' => true, + 'useSystemCredentials' => false, + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + 'promptsCount' => 42, + ], + ]), ]); $provider = $this->crowdin->ai->createProvider(1, $params); + $this->assertInstanceOf(AiProvider::class, $provider); $this->assertEquals(2, $provider->getId()); $this->assertEquals('OpenAI', $provider->getName()); @@ -563,30 +565,33 @@ public function testGetProvider(): void $this->mockRequest([ 'path' => '/users/1/ai/providers/2', 'method' => 'get', - 'response' => '{ - "data": { - "id": 2, - "name": "OpenAI", - "type": "open_ai", - "credentials": {"apiKey": "string"}, - "config": { - "actionRules": [ - { - "action": "pre_translate", - "availableAiModelIds": ["gpt-5.4"] - } - ] - }, - "isEnabled": true, - "useSystemCredentials": false, - "createdAt": "2019-09-20T11:11:05+00:00", - "updatedAt": "2019-09-20T12:22:20+00:00", - "promptsCount": 42 - } - }', + 'response' => json_encode([ + 'data' => [ + 'id' => 2, + 'name' => 'OpenAI', + 'type' => 'open_ai', + 'credentials' => [ + 'apiKey' => 'string', + ], + 'config' => [ + 'actionRules' => [ + [ + 'action' => 'pre_translate', + 'availableAiModelIds' => ['gpt-5.4'], + ], + ], + ], + 'isEnabled' => true, + 'useSystemCredentials' => false, + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + 'promptsCount' => 42, + ], + ]), ]); $provider = $this->crowdin->ai->getProvider(1, 2); + $this->assertInstanceOf(AiProvider::class, $provider); $this->assertEquals(2, $provider->getId()); $this->assertFalse($provider->isUseSystemCredentials()); @@ -614,22 +619,26 @@ public function testListProviderModels(): void $this->mockRequest([ 'path' => '/users/1/ai/providers/2/models', 'method' => 'get', - 'response' => '{ - "data": [ - { - "data": { - "id": "gpt-5.4", - "provider": "open_ai", - "providerName": "OpenAI", - "providerId": 1 - } - } - ], - "pagination": {"offset": 0, "limit": 25} - }', + 'response' => json_encode([ + 'data' => [ + [ + 'data' => [ + 'id' => 'gpt-5.4', + 'provider' => 'open_ai', + 'providerName' => 'OpenAI', + 'providerId' => 1, + ], + ], + ], + 'pagination' => [ + 'offset' => 0, + 'limit' => 25, + ], + ]), ]); $models = $this->crowdin->ai->listProviderModels(1, 2); + $this->assertInstanceOf(ModelCollection::class, $models); $this->assertInstanceOf(AiProviderModel::class, $models[0]); $this->assertEquals('gpt-5.4', $models[0]->getId()); @@ -651,20 +660,48 @@ public function testCreateProviderChatCompletion(): void 'path' => '/users/1/ai/providers/2/chat/completions', 'method' => 'post', 'body' => json_encode($params), - 'response' => '{ - "data": { - "id": "chatcmpl-123", - "object": "chat.completion", - "model": "gpt-4o", - "choices": [{"message": {"role": "assistant", "content": "Hi!"}}] - } - }', + 'response' => json_encode([ + 'data' => [ + 'id' => 'chatcmpl-123', + 'object' => 'chat.completion', + 'model' => 'gpt-4o', + 'choices' => [ + [ + 'message' => [ + 'role' => 'assistant', + 'content' => 'Hi!', + ], + ], + ], + ], + ]), ]); $completion = $this->crowdin->ai->createProviderChatCompletion(1, 2, $params); + $this->assertInstanceOf(AiProxyChatCompletion::class, $completion); } + private function reportResponse(): string + { + return json_encode([ + 'data' => [ + 'identifier' => '50fb3506-4127-4ba8-8296-f97dc7e3e0c3', + 'status' => 'finished', + 'progress' => 100, + 'attributes' => [ + 'format' => 'json', + 'reportType' => 'tokens-usage-raw-data', + 'schema' => [], + ], + 'createdAt' => '2024-01-23T11:26:54+00:00', + 'updatedAt' => '2024-09-23T11:26:54+00:00', + 'startedAt' => '2024-05-23T11:26:54+00:00', + 'finishedAt' => '2024-05-23T11:26:54+00:00', + ], + ]); + } + public function testGenerateReport(): void { $params = [ @@ -683,25 +720,11 @@ public function testGenerateReport(): void 'path' => '/users/1/ai/reports', 'method' => 'post', 'body' => json_encode($params), - 'response' => '{ - "data": { - "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", - "status": "finished", - "progress": 100, - "attributes": { - "format": "json", - "reportType": "tokens-usage-raw-data", - "schema": {} - }, - "createdAt": "2024-01-23T11:26:54+00:00", - "updatedAt": "2024-09-23T11:26:54+00:00", - "startedAt": "2024-05-23T11:26:54+00:00", - "finishedAt": "2024-05-23T11:26:54+00:00" - } - }', + 'response' => $this->reportResponse(), ]); $report = $this->crowdin->ai->generateReport(1, $params); + $this->assertInstanceOf(AiReport::class, $report); $this->assertEquals('50fb3506-4127-4ba8-8296-f97dc7e3e0c3', $report->getIdentifier()); $this->assertEquals('finished', $report->getStatus()); @@ -712,25 +735,11 @@ public function testGetReport(): void $this->mockRequest([ 'path' => '/users/1/ai/reports/50fb3506-4127-4ba8-8296-f97dc7e3e0c3', 'method' => 'get', - 'response' => '{ - "data": { - "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", - "status": "finished", - "progress": 100, - "attributes": { - "format": "json", - "reportType": "tokens-usage-raw-data", - "schema": {} - }, - "createdAt": "2024-01-23T11:26:54+00:00", - "updatedAt": "2024-09-23T11:26:54+00:00", - "startedAt": "2024-05-23T11:26:54+00:00", - "finishedAt": "2024-05-23T11:26:54+00:00" - } - }', + 'response' => $this->reportResponse(), ]); $report = $this->crowdin->ai->getReport(1, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + $this->assertInstanceOf(AiReport::class, $report); $this->assertEquals('finished', $report->getStatus()); $this->assertEquals(100, $report->getProgress()); @@ -746,15 +755,11 @@ public function testDownloadReport(): void $this->mockRequest([ 'path' => '/users/1/ai/reports/50fb3506-4127-4ba8-8296-f97dc7e3e0c3/download', 'method' => 'get', - 'response' => '{ - "data": { - "url": "https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff", - "expireIn": "2019-09-20T10:31:21+00:00" - } - }', + 'response' => $this->downloadFileResponse(), ]); $download = $this->crowdin->ai->downloadReport(1, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + $this->assertInstanceOf(DownloadFile::class, $download); $this->assertEquals('https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff', $download->getUrl()); $this->assertEquals('2019-09-20T10:31:21+00:00', $download->getExpireIn()); @@ -835,17 +840,18 @@ public function testGetSettings(): void $this->mockRequest([ 'path' => '/users/1/ai/settings', 'method' => 'get', - 'response' => '{ - "data": { - "preTranslationAiPromptId": 2, - "editorSuggestionAiPromptId": 5, - "qaCheckActionAiPromptId": 8, - "contextReviewAiPromptId": 11 - } - }', + 'response' => json_encode([ + 'data' => [ + 'preTranslationAiPromptId' => 2, + 'editorSuggestionAiPromptId' => 5, + 'qaCheckActionAiPromptId' => 8, + 'contextReviewAiPromptId' => 11, + ], + ]), ]); $settings = $this->crowdin->ai->getSettings(1); + $this->assertInstanceOf(AiSettings::class, $settings); $this->assertEquals(2, $settings->getPreTranslationAiPromptId()); $this->assertEquals(5, $settings->getEditorSuggestionAiPromptId()); @@ -858,24 +864,28 @@ public function testListSnippets(): void $this->mockRequest([ 'path' => '/users/1/ai/settings/snippets', 'method' => 'get', - 'response' => '{ - "data": [ - { - "data": { - "id": 2, - "description": "Product description", - "placeholder": "%custom:productDescription%", - "value": "The product is the professional consulting service that transform challenges into opportunities.", - "createdAt": "2019-09-20T11:11:05+00:00", - "updatedAt": "2019-09-20T12:22:20+00:00" - } - } - ], - "pagination": {"offset": 0, "limit": 25} - }', + 'response' => json_encode([ + 'data' => [ + [ + 'data' => [ + 'id' => 2, + 'description' => 'Product description', + 'placeholder' => '%custom:productDescription%', + 'value' => 'The product is the professional consulting service that transform challenges into opportunities.', + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + ], + ], + ], + 'pagination' => [ + 'offset' => 0, + 'limit' => 25, + ], + ]), ]); $snippets = $this->crowdin->ai->listSnippets(1); + $this->assertInstanceOf(ModelCollection::class, $snippets); $this->assertInstanceOf(AiSnippet::class, $snippets[0]); $this->assertEquals(2, $snippets[0]->getId()); @@ -894,19 +904,11 @@ public function testCreateSnippet(): void 'path' => '/users/1/ai/settings/snippets', 'method' => 'post', 'body' => json_encode($params), - 'response' => '{ - "data": { - "id": 2, - "description": "Product description", - "placeholder": "%custom:productDescription%", - "value": "The product is the professional consulting service that transform challenges into opportunities.", - "createdAt": "2019-09-20T11:11:05+00:00", - "updatedAt": "2019-09-20T12:22:20+00:00" - } - }', + 'response' => $this->snippetResponse(), ]); $snippet = $this->crowdin->ai->createSnippet(1, $params); + $this->assertInstanceOf(AiSnippet::class, $snippet); $this->assertEquals(2, $snippet->getId()); $this->assertEquals('Product description', $snippet->getDescription()); @@ -917,19 +919,11 @@ public function testGetSnippet(): void $this->mockRequest([ 'path' => '/users/1/ai/settings/snippets/2', 'method' => 'get', - 'response' => '{ - "data": { - "id": 2, - "description": "Product description", - "placeholder": "%custom:productDescription%", - "value": "The product is the professional consulting service that transform challenges into opportunities.", - "createdAt": "2019-09-20T11:11:05+00:00", - "updatedAt": "2019-09-20T12:22:20+00:00" - } - }', + 'response' => $this->snippetResponse(), ]); $snippet = $this->crowdin->ai->getSnippet(1, 2); + $this->assertInstanceOf(AiSnippet::class, $snippet); $this->assertEquals( 'The product is the professional consulting service that transform challenges into opportunities.', @@ -939,6 +933,20 @@ public function testGetSnippet(): void $this->assertEquals('2019-09-20T12:22:20+00:00', $snippet->getUpdatedAt()); } + private function snippetResponse(): string + { + return json_encode([ + 'data' => [ + 'id' => 2, + 'description' => 'Product description', + 'placeholder' => '%custom:productDescription%', + 'value' => 'The product is the professional consulting service that transform challenges into opportunities.', + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + ], + ]); + } + public function testDeleteSnippet(): void { $this->mockRequest([ @@ -1004,6 +1012,7 @@ public function testUpdatePrompt(): void ); $updated = $this->crowdin->ai->updatePrompt(1, $prompt); + $this->assertInstanceOf(AiPrompt::class, $updated); $this->assertEquals(2, $updated->getId()); $this->assertEquals('Updated prompt', $updated->getName()); @@ -1050,6 +1059,7 @@ public function testUpdateProvider(): void ); $updated = $this->crowdin->ai->updateProvider(1, $provider); + $this->assertInstanceOf(AiProvider::class, $updated); $this->assertEquals(2, $updated->getId()); $this->assertEquals('Azure OpenAI', $updated->getName()); @@ -1081,6 +1091,7 @@ public function testUpdateSettings(): void ); $updated = $this->crowdin->ai->updateSettings(1, $settings); + $this->assertInstanceOf(AiSettings::class, $updated); $this->assertEquals(3, $updated->getPreTranslationAiPromptId()); $this->assertEquals(6, $updated->getEditorSuggestionAiPromptId()); @@ -1117,9 +1128,486 @@ public function testUpdateSnippet(): void ); $updated = $this->crowdin->ai->updateSnippet(1, $snippet); + $this->assertInstanceOf(AiSnippet::class, $updated); $this->assertEquals('Updated description', $updated->getDescription()); $this->assertEquals('%custom:updatedDescription%', $updated->getPlaceholder()); $this->assertEquals('Updated value.', $updated->getValue()); } + + public function testGetProjectSettings(): void + { + $this->mockRequest([ + 'path' => '/projects/2/ai/settings', + 'method' => 'get', + 'response' => json_encode([ + 'data' => [ + 'editorSuggestionAiPromptId' => 6, + 'alignmentActionAiPromptId' => null, + 'qaCheckActionAiPromptId' => 9, + 'contextReviewAiPromptId' => 12, + ], + ]), + ]); + + $result = $this->crowdin->ai->getProjectSettings(2); + + $this->assertInstanceOf(ProjectAiSettings::class, $result); + $this->assertEquals(6, $result->getEditorSuggestionAiPromptId()); + $this->assertNull($result->getAlignmentActionAiPromptId()); + } + + public function testGenerateFineTuningDataset(): void + { + $data = [ + 'projectIds' => [1], + 'tmIds' => [2], + 'purpose' => 'training', + ]; + + $this->mockRequest([ + 'path' => '/users/1/ai/prompts/3/fine-tuning/datasets', + 'method' => 'post', + 'body' => json_encode($data), + 'response' => $this->fineTuningDatasetResponse(), + ]); + + $result = $this->crowdin->ai->generateFineTuningDataset(1, 3, $data); + + $this->assertInstanceOf(AiFineTuningDataset::class, $result); + $this->assertEquals('1d5b5a0b-6b1c-4f1a-9f3b-2b1e1c3d4e5f', $result->getIdentifier()); + $this->assertEquals('training', $result->getAttributes()['purpose']); + } + + public function testGetFineTuningDataset(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/prompts/3/fine-tuning/datasets/1d5b5a0b-6b1c-4f1a-9f3b-2b1e1c3d4e5f', + 'method' => 'get', + 'response' => $this->fineTuningDatasetResponse(), + ]); + + $result = $this->crowdin->ai->getFineTuningDataset(1, 3, '1d5b5a0b-6b1c-4f1a-9f3b-2b1e1c3d4e5f'); + + $this->assertInstanceOf(AiFineTuningDataset::class, $result); + $this->assertEquals('finished', $result->getStatus()); + } + + private function fineTuningDatasetResponse(): string + { + return json_encode([ + 'data' => [ + 'identifier' => '1d5b5a0b-6b1c-4f1a-9f3b-2b1e1c3d4e5f', + 'status' => 'finished', + 'progress' => 100, + 'attributes' => [ + 'projectIds' => [1], + 'tmIds' => [2], + 'purpose' => 'training', + ], + 'createdAt' => '2025-09-23T11:26:54+00:00', + 'updatedAt' => '2025-09-23T11:26:54+00:00', + 'startedAt' => '2025-09-23T11:26:54+00:00', + 'finishedAt' => '2025-09-23T11:26:54+00:00', + ], + ]); + } + + public function testDownloadFineTuningDataset(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/prompts/3/fine-tuning/datasets/1d5b5a0b-6b1c-4f1a-9f3b-2b1e1c3d4e5f/download', + 'method' => 'get', + 'response' => $this->jsonlDownloadResponse(), + ]); + + $result = $this->crowdin->ai->downloadFineTuningDataset(1, 3, '1d5b5a0b-6b1c-4f1a-9f3b-2b1e1c3d4e5f'); + + $this->assertInstanceOf(DownloadFile::class, $result); + $this->assertEquals('https://production-enterprise-importer.downloads.crowdin.com/file.jsonl', $result->getUrl()); + } + + private function jsonlDownloadResponse(): string + { + return json_encode([ + 'data' => [ + 'url' => 'https://production-enterprise-importer.downloads.crowdin.com/file.jsonl', + 'expireIn' => '2025-09-20T10:31:21+00:00', + ], + ]); + } + + public function testListFineTuningJobs(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/prompts/fine-tuning/jobs?statuses=in_progress', + 'method' => 'get', + 'response' => json_encode([ + 'data' => [ + [ + 'data' => [ + 'identifier' => 'a1b2c3d4-0000-4000-8000-000000000001', + 'status' => 'in_progress', + 'progress' => 50, + 'attributes' => [ + 'dryRun' => false, + 'aiPromptId' => 3, + 'trainingOptions' => [ + 'projectIds' => [1], + ], + 'fineTunedModel' => null, + ], + 'createdAt' => '2025-09-23T11:26:54+00:00', + 'updatedAt' => '2025-09-23T11:26:54+00:00', + 'startedAt' => '2025-09-23T11:26:54+00:00', + 'finishedAt' => null, + ], + ], + ], + 'pagination' => [ + 'offset' => 0, + 'limit' => 25, + ], + ]), + ]); + + $result = $this->crowdin->ai->listFineTuningJobs(1, ['statuses' => 'in_progress']); + + $this->assertInstanceOf(ModelCollection::class, $result); + $this->assertCount(1, $result); + $this->assertInstanceOf(AiFineTuningJob::class, $result[0]); + $this->assertEquals('in_progress', $result[0]->getStatus()); + } + + public function testCreateFineTuningJob(): void + { + $data = [ + 'dryRun' => false, + 'hyperparameters' => [ + 'nEpochs' => 3, + ], + 'trainingOptions' => [ + 'projectIds' => [1], + ], + ]; + + $this->mockRequest([ + 'path' => '/users/1/ai/prompts/3/fine-tuning/jobs', + 'method' => 'post', + 'body' => json_encode($data), + 'response' => $this->fineTuningJobResponse(), + ]); + + $result = $this->crowdin->ai->createFineTuningJob(1, 3, $data); + + $this->assertInstanceOf(AiFineTuningJob::class, $result); + $this->assertEquals(3, $result->getAttributes()['aiPromptId']); + } + + public function testGetFineTuningJob(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/prompts/3/fine-tuning/jobs/a1b2c3d4-0000-4000-8000-000000000001', + 'method' => 'get', + 'response' => $this->fineTuningJobResponse(), + ]); + + $result = $this->crowdin->ai->getFineTuningJob(1, 3, 'a1b2c3d4-0000-4000-8000-000000000001'); + + $this->assertInstanceOf(AiFineTuningJob::class, $result); + $this->assertEquals(50, $result->getProgress()); + $this->assertNull($result->getFinishedAt()); + } + + private function fineTuningJobResponse(): string + { + return json_encode([ + 'data' => [ + 'identifier' => 'a1b2c3d4-0000-4000-8000-000000000001', + 'status' => 'in_progress', + 'progress' => 50, + 'attributes' => [ + 'dryRun' => false, + 'aiPromptId' => 3, + 'trainingOptions' => [ + 'projectIds' => [1], + ], + 'fineTunedModel' => null, + ], + 'createdAt' => '2025-09-23T11:26:54+00:00', + 'updatedAt' => '2025-09-23T11:26:54+00:00', + 'startedAt' => '2025-09-23T11:26:54+00:00', + 'finishedAt' => null, + ], + ]); + } + + public function testListFineTuningEvents(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/prompts/3/fine-tuning/jobs/a1b2c3d4-0000-4000-8000-000000000001/events', + 'method' => 'get', + 'response' => json_encode([ + 'data' => [ + [ + 'data' => [ + 'id' => 'ftevent-1', + 'type' => 'metrics', + 'message' => 'Step 1/10', + 'data' => [ + 'step' => 1, + 'totalSteps' => 10, + 'trainingLoss' => 0.5, + ], + 'createdAt' => '2025-09-23T11:26:54+00:00', + ], + ], + ], + 'pagination' => [ + 'offset' => 0, + 'limit' => 25, + ], + ]), + ]); + + $result = $this->crowdin->ai->listFineTuningEvents(1, 3, 'a1b2c3d4-0000-4000-8000-000000000001'); + + $this->assertInstanceOf(ModelCollection::class, $result); + $this->assertCount(1, $result); + $this->assertInstanceOf(AiFineTuningEvent::class, $result[0]); + $this->assertEquals('metrics', $result[0]->getType()); + $this->assertEquals(10, $result[0]->getEventData()['totalSteps']); + } + + public function testListAllProviderModels(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/providers/models', + 'method' => 'get', + 'response' => json_encode([ + 'data' => [ + [ + 'data' => [ + 'id' => 'gpt-4.1', + 'provider' => 'open_ai', + 'providerName' => 'OpenAI', + 'providerId' => 2, + 'contextWindow' => 128000, + 'maxOutputTokens' => 16384, + 'supportsStreaming' => true, + 'supportsFunctionCalling' => true, + 'supportsJsonMode' => true, + 'supportsJsonSchema' => true, + 'supportsVision' => false, + 'isCompatibleWithAiLimit' => true, + ], + ], + ], + 'pagination' => [ + 'offset' => 0, + 'limit' => 25, + ], + ]), + ]); + + $result = $this->crowdin->ai->listAllProviderModels(1); + + $this->assertInstanceOf(ModelCollection::class, $result); + $this->assertCount(1, $result); + $this->assertInstanceOf(AiProviderModel::class, $result[0]); + $this->assertEquals(128000, $result[0]->getContextWindow()); + $this->assertTrue($result[0]->getIsCompatibleWithAiLimit()); + } + + public function testListSupportedProviderModels(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/providers/supported-models?providerType=open_ai', + 'method' => 'get', + 'response' => json_encode([ + 'data' => [ + [ + 'data' => [ + 'providerId' => 2, + 'providerType' => 'open_ai', + 'providerName' => 'OpenAI', + 'id' => 'gpt-4.1', + 'displayName' => 'GPT-4.1', + 'supportReasoning' => false, + 'intelligence' => 4, + 'speed' => 3, + 'price' => [ + 'input' => 2.0, + 'output' => 8.0, + ], + 'modalities' => [ + 'input' => ['text'], + 'output' => ['text'], + ], + 'contextWindow' => 128000, + 'maxOutputTokens' => 16384, + 'knowledgeCutoff' => '2024-06', + 'releaseDate' => '2025-04-14', + 'features' => [ + 'streaming' => true, + 'structuredOutput' => true, + 'functionCalling' => true, + ], + ], + ], + ], + 'pagination' => [ + 'offset' => 0, + 'limit' => 25, + ], + ]), + ]); + + $result = $this->crowdin->ai->listSupportedProviderModels(1, ['providerType' => 'open_ai']); + + $this->assertInstanceOf(ModelCollection::class, $result); + $this->assertCount(1, $result); + $this->assertInstanceOf(AiSupportedModel::class, $result[0]); + $this->assertEquals('GPT-4.1', $result[0]->getDisplayName()); + } + + public function testExportRequestLogs(): void + { + $data = [ + 'format' => 'csv', + 'projectId' => 1, + ]; + + $this->mockRequest([ + 'path' => '/users/1/ai/request-logs/exports', + 'method' => 'post', + 'body' => json_encode($data), + 'response' => $this->requestLogExportResponse(), + ]); + + $result = $this->crowdin->ai->exportRequestLogs(1, $data); + + $this->assertInstanceOf(AiRequestLogExport::class, $result); + $this->assertEquals('created', $result->getStatus()); + $this->assertEquals('10 seconds', $result->getEta()); + } + + public function testGetRequestLogsExport(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/request-logs/exports/e7c1d7f2-1111-4222-8333-444455556666', + 'method' => 'get', + 'response' => $this->requestLogExportResponse(), + ]); + + $result = $this->crowdin->ai->getRequestLogsExport(1, 'e7c1d7f2-1111-4222-8333-444455556666'); + + $this->assertInstanceOf(AiRequestLogExport::class, $result); + $this->assertEquals('e7c1d7f2-1111-4222-8333-444455556666', $result->getIdentifier()); + } + + private function requestLogExportResponse(): string + { + return json_encode([ + 'data' => [ + 'identifier' => 'e7c1d7f2-1111-4222-8333-444455556666', + 'status' => 'created', + 'progress' => 0, + 'attributes' => [ + 'format' => 'csv', + 'filters' => [ + 'projectId' => 1, + ], + ], + 'createdAt' => '2025-09-23T11:26:54+00:00', + 'updatedAt' => '2025-09-23T11:26:54+00:00', + 'startedAt' => null, + 'finishedAt' => null, + 'eta' => '10 seconds', + ], + ]); + } + + public function testDownloadRequestLogsExport(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/request-logs/exports/e7c1d7f2-1111-4222-8333-444455556666/download', + 'method' => 'get', + 'response' => $this->jsonlDownloadResponse(), + ]); + + $result = $this->crowdin->ai->downloadRequestLogsExport(1, 'e7c1d7f2-1111-4222-8333-444455556666'); + + $this->assertInstanceOf(DownloadFile::class, $result); + $this->assertEquals('2025-09-20T10:31:21+00:00', $result->getExpireIn()); + } + + public function testListUsageMembers(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/usage/members?limit=10', + 'method' => 'get', + 'response' => json_encode([ + 'data' => [ + [ + 'data' => [ + 'user' => [ + 'id' => 12, + 'username' => 'john', + 'fullName' => 'John Smith', + 'avatarUrl' => '', + ], + 'dailyCostLimit' => 5, + 'dailyCostSpent' => 1.25, + 'dailyResetAt' => '2025-09-24T00:00:00+00:00', + 'monthlyCostLimit' => null, + 'monthlyCostSpent' => 10.5, + 'monthlyResetAt' => '2025-10-01T00:00:00+00:00', + ], + ], + ], + 'pagination' => [ + 'offset' => 0, + 'limit' => 25, + ], + ]), + ]); + + $result = $this->crowdin->ai->listUsageMembers(1, ['limit' => 10]); + + $this->assertInstanceOf(ModelCollection::class, $result); + $this->assertCount(1, $result); + $this->assertInstanceOf(AiMemberUsage::class, $result[0]); + $this->assertEquals(12, $result[0]->getUser()['id']); + } + + public function testGetUsageMember(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/usage/members/12', + 'method' => 'get', + 'response' => json_encode([ + 'data' => [ + 'user' => [ + 'id' => 12, + 'username' => 'john', + 'fullName' => 'John Smith', + 'avatarUrl' => '', + ], + 'dailyCostLimit' => 5, + 'dailyCostSpent' => 1.25, + 'dailyResetAt' => '2025-09-24T00:00:00+00:00', + 'monthlyCostLimit' => null, + 'monthlyCostSpent' => 10.5, + 'monthlyResetAt' => '2025-10-01T00:00:00+00:00', + ], + ]), + ]); + + $result = $this->crowdin->ai->getUsageMember(1, 12); + + $this->assertInstanceOf(AiMemberUsage::class, $result); + $this->assertEquals(5.0, $result->getDailyCostLimit()); + $this->assertNull($result->getMonthlyCostLimit()); + } } diff --git a/tests/CrowdinApiClient/Api/Enterprise/AiApiTest.php b/tests/CrowdinApiClient/Api/Enterprise/AiApiTest.php index 81b966aa..4b78df40 100644 --- a/tests/CrowdinApiClient/Api/Enterprise/AiApiTest.php +++ b/tests/CrowdinApiClient/Api/Enterprise/AiApiTest.php @@ -3,6 +3,10 @@ namespace CrowdinApiClient\Tests\Api\Enterprise; use CrowdinApiClient\Model\AiFileTranslation; +use CrowdinApiClient\Model\AiFineTuningDataset; +use CrowdinApiClient\Model\AiFineTuningEvent; +use CrowdinApiClient\Model\AiFineTuningJob; +use CrowdinApiClient\Model\AiMemberUsage; use CrowdinApiClient\Model\AiPrompt; use CrowdinApiClient\Model\AiPromptCompletion; use CrowdinApiClient\Model\AiProvider; @@ -10,10 +14,13 @@ use CrowdinApiClient\Model\AiProxyChatCompletion; use CrowdinApiClient\Model\AiReport; use CrowdinApiClient\Model\AiRequestLog; +use CrowdinApiClient\Model\AiRequestLogExport; use CrowdinApiClient\Model\AiSettings; use CrowdinApiClient\Model\AiSnippet; +use CrowdinApiClient\Model\AiSupportedModel; use CrowdinApiClient\Model\AiTranslation; use CrowdinApiClient\Model\DownloadFile; +use CrowdinApiClient\Model\ProjectAiSettings; use CrowdinApiClient\ModelCollection; class AiApiTest extends AbstractTestApi @@ -30,19 +37,17 @@ public function testTranslateStrings(): void 'path' => '/ai/translate', 'method' => 'post', 'body' => json_encode($params), - 'response' => '{ - "data": { - "sourceLanguageId": "en", - "targetLanguageId": "uk", - "translations": [ - "Перекладений текст 1", - "Перекладений текст 2" - ] - } - }', + 'response' => json_encode([ + 'data' => [ + 'sourceLanguageId' => 'en', + 'targetLanguageId' => 'uk', + 'translations' => ['Перекладений текст 1', 'Перекладений текст 2'], + ], + ]), ]); $aiTranslation = $this->crowdin->ai->translateStrings($params); + $this->assertInstanceOf(AiTranslation::class, $aiTranslation); $this->assertEquals('en', $aiTranslation->getSourceLanguageId()); $this->assertEquals('uk', $aiTranslation->getTargetLanguageId()); @@ -69,30 +74,11 @@ public function testCreateFileTranslation(): void 'path' => '/ai/file-translations', 'method' => 'post', 'body' => json_encode($params), - 'response' => '{ - "data": { - "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", - "status": "finished", - "progress": 100, - "attributes": { - "stage": "translate", - "error": null, - "downloadName": "file.pdf", - "sourceLanguageId": "en", - "targetLanguageId": "uk", - "originalFileName": "Sample_Chrome.json", - "detectedType": "chrome", - "parserVersion": 2 - }, - "createdAt": "2026-01-23T11:26:54+00:00", - "updatedAt": "2026-01-23T11:26:54+00:00", - "startedAt": "2026-01-23T11:26:54+00:00", - "finishedAt": "2026-01-23T11:26:54+00:00" - } - }', + 'response' => $this->fileTranslationResponse(), ]); $fileTranslation = $this->crowdin->ai->createFileTranslation($params); + $this->assertInstanceOf(AiFileTranslation::class, $fileTranslation); $this->assertEquals('50fb3506-4127-4ba8-8296-f97dc7e3e0c3', $fileTranslation->getIdentifier()); $this->assertEquals('finished', $fileTranslation->getStatus()); @@ -105,30 +91,11 @@ public function testGetFileTranslation(): void $this->mockRequest([ 'path' => '/ai/file-translations/50fb3506-4127-4ba8-8296-f97dc7e3e0c3', 'method' => 'get', - 'response' => '{ - "data": { - "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", - "status": "finished", - "progress": 100, - "attributes": { - "stage": "translate", - "error": null, - "downloadName": "file.pdf", - "sourceLanguageId": "en", - "targetLanguageId": "uk", - "originalFileName": "Sample_Chrome.json", - "detectedType": "chrome", - "parserVersion": 2 - }, - "createdAt": "2026-01-23T11:26:54+00:00", - "updatedAt": "2026-01-23T11:26:54+00:00", - "startedAt": "2026-01-23T11:26:54+00:00", - "finishedAt": "2026-01-23T11:26:54+00:00" - } - }', + 'response' => $this->fileTranslationResponse(), ]); $fileTranslation = $this->crowdin->ai->getFileTranslation('50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + $this->assertInstanceOf(AiFileTranslation::class, $fileTranslation); $this->assertEquals('50fb3506-4127-4ba8-8296-f97dc7e3e0c3', $fileTranslation->getIdentifier()); $this->assertEquals('finished', $fileTranslation->getStatus()); @@ -139,6 +106,31 @@ public function testGetFileTranslation(): void $this->assertEquals('2026-01-23T11:26:54+00:00', $fileTranslation->getFinishedAt()); } + private function fileTranslationResponse(): string + { + return json_encode([ + 'data' => [ + 'identifier' => '50fb3506-4127-4ba8-8296-f97dc7e3e0c3', + 'status' => 'finished', + 'progress' => 100, + 'attributes' => [ + 'stage' => 'translate', + 'error' => null, + 'downloadName' => 'file.pdf', + 'sourceLanguageId' => 'en', + 'targetLanguageId' => 'uk', + 'originalFileName' => 'Sample_Chrome.json', + 'detectedType' => 'chrome', + 'parserVersion' => 2, + ], + 'createdAt' => '2026-01-23T11:26:54+00:00', + 'updatedAt' => '2026-01-23T11:26:54+00:00', + 'startedAt' => '2026-01-23T11:26:54+00:00', + 'finishedAt' => '2026-01-23T11:26:54+00:00', + ], + ]); + } + public function testDeleteFileTranslation(): void { $this->mockRequest([ @@ -150,20 +142,26 @@ public function testDeleteFileTranslation(): void $this->crowdin->ai->deleteFileTranslation('50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); } + private function downloadFileResponse(): string + { + return json_encode([ + 'data' => [ + 'url' => 'https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff', + 'expireIn' => '2019-09-20T10:31:21+00:00', + ], + ]); + } + public function testDownloadFileTranslation(): void { $this->mockRequest([ 'path' => '/ai/file-translations/50fb3506-4127-4ba8-8296-f97dc7e3e0c3/download', 'method' => 'get', - 'response' => '{ - "data": { - "url": "https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff", - "expireIn": "2019-09-20T10:31:21+00:00" - } - }', + 'response' => $this->downloadFileResponse(), ]); $downloadFile = $this->crowdin->ai->downloadFileTranslation('50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + $this->assertInstanceOf(DownloadFile::class, $downloadFile); $this->assertEquals('https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff', $downloadFile->getUrl()); $this->assertEquals('2019-09-20T10:31:21+00:00', $downloadFile->getExpireIn()); @@ -174,15 +172,11 @@ public function testDownloadFileTranslationStrings(): void $this->mockRequest([ 'path' => '/ai/file-translations/50fb3506-4127-4ba8-8296-f97dc7e3e0c3/translations', 'method' => 'get', - 'response' => '{ - "data": { - "url": "https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff", - "expireIn": "2019-09-20T10:31:21+00:00" - } - }', + 'response' => $this->downloadFileResponse(), ]); $downloadFile = $this->crowdin->ai->downloadFileTranslationStrings('50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + $this->assertInstanceOf(DownloadFile::class, $downloadFile); $this->assertEquals('https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff', $downloadFile->getUrl()); $this->assertEquals('2019-09-20T10:31:21+00:00', $downloadFile->getExpireIn()); @@ -193,35 +187,39 @@ public function testListPrompts(): void $this->mockRequest([ 'path' => '/ai/prompts', 'method' => 'get', - 'response' => '{ - "data": [ - { - "data": { - "id": 2, - "name": "Pre-translate prompt", - "action": "pre_translate", - "aiProviderId": 2, - "aiModelId": "gpt-5.4", - "isEnabled": true, - "enabledProjectIds": [1], - "config": {}, - "promptPreview": null, - "isFineTuningAvailable": true, - "createdBy": 123, - "updatedBy": 456, - "lastUsedBy": 789, - "lastUsedAt": "2019-09-25T14:30:00+00:00", - "usageCount": 42, - "createdAt": "2019-09-20T11:11:05+00:00", - "updatedAt": "2019-09-20T12:22:20+00:00" - } - } - ], - "pagination": {"offset": 0, "limit": 25} - }', + 'response' => json_encode([ + 'data' => [ + [ + 'data' => [ + 'id' => 2, + 'name' => 'Pre-translate prompt', + 'action' => 'pre_translate', + 'aiProviderId' => 2, + 'aiModelId' => 'gpt-5.4', + 'isEnabled' => true, + 'enabledProjectIds' => [1], + 'config' => [], + 'promptPreview' => null, + 'isFineTuningAvailable' => true, + 'createdBy' => 123, + 'updatedBy' => 456, + 'lastUsedBy' => 789, + 'lastUsedAt' => '2019-09-25T14:30:00+00:00', + 'usageCount' => 42, + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + ], + ], + ], + 'pagination' => [ + 'offset' => 0, + 'limit' => 25, + ], + ]), ]); $prompts = $this->crowdin->ai->listPrompts(); + $this->assertInstanceOf(ModelCollection::class, $prompts); $this->assertInstanceOf(AiPrompt::class, $prompts[0]); $this->assertEquals(2, $prompts[0]->getId()); @@ -240,30 +238,33 @@ public function testCreatePrompt(): void 'path' => '/ai/prompts', 'method' => 'post', 'body' => json_encode($params), - 'response' => '{ - "data": { - "id": 2, - "name": "Pre-translate prompt", - "action": "pre_translate", - "aiProviderId": 2, - "aiModelId": "gpt-5.4", - "isEnabled": true, - "enabledProjectIds": [1], - "config": {"mode": "basic"}, - "promptPreview": null, - "isFineTuningAvailable": true, - "createdBy": 123, - "updatedBy": 456, - "lastUsedBy": 789, - "lastUsedAt": "2019-09-25T14:30:00+00:00", - "usageCount": 42, - "createdAt": "2019-09-20T11:11:05+00:00", - "updatedAt": "2019-09-20T12:22:20+00:00" - } - }', + 'response' => json_encode([ + 'data' => [ + 'id' => 2, + 'name' => 'Pre-translate prompt', + 'action' => 'pre_translate', + 'aiProviderId' => 2, + 'aiModelId' => 'gpt-5.4', + 'isEnabled' => true, + 'enabledProjectIds' => [1], + 'config' => [ + 'mode' => 'basic', + ], + 'promptPreview' => null, + 'isFineTuningAvailable' => true, + 'createdBy' => 123, + 'updatedBy' => 456, + 'lastUsedBy' => 789, + 'lastUsedAt' => '2019-09-25T14:30:00+00:00', + 'usageCount' => 42, + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + ], + ]), ]); $prompt = $this->crowdin->ai->createPrompt($params); + $this->assertInstanceOf(AiPrompt::class, $prompt); $this->assertEquals(2, $prompt->getId()); $this->assertEquals('Pre-translate prompt', $prompt->getName()); @@ -274,36 +275,37 @@ public function testGetPrompt(): void $this->mockRequest([ 'path' => '/ai/prompts/2', 'method' => 'get', - 'response' => '{ - "data": { - "id": 2, - "name": "Pre-translate prompt", - "action": "pre_translate", - "aiProviderId": 2, - "aiModelId": "gpt-5.4", - "isEnabled": true, - "enabledProjectIds": [1], - "config": { - "mode": "basic", - "snippets": ["%custom:companyDescription%"], - "glossaryTerms": true, - "tmSuggestions": true, - "fileContext": true - }, - "promptPreview": null, - "isFineTuningAvailable": true, - "createdBy": 123, - "updatedBy": 456, - "lastUsedBy": 789, - "lastUsedAt": "2019-09-25T14:30:00+00:00", - "usageCount": 42, - "createdAt": "2019-09-20T11:11:05+00:00", - "updatedAt": "2019-09-20T12:22:20+00:00" - } - }', + 'response' => json_encode([ + 'data' => [ + 'id' => 2, + 'name' => 'Pre-translate prompt', + 'action' => 'pre_translate', + 'aiProviderId' => 2, + 'aiModelId' => 'gpt-5.4', + 'isEnabled' => true, + 'enabledProjectIds' => [1], + 'config' => [ + 'mode' => 'basic', + 'snippets' => ['%custom:companyDescription%'], + 'glossaryTerms' => true, + 'tmSuggestions' => true, + 'fileContext' => true, + ], + 'promptPreview' => null, + 'isFineTuningAvailable' => true, + 'createdBy' => 123, + 'updatedBy' => 456, + 'lastUsedBy' => 789, + 'lastUsedAt' => '2019-09-25T14:30:00+00:00', + 'usageCount' => 42, + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + ], + ]), ]); $prompt = $this->crowdin->ai->getPrompt(2); + $this->assertInstanceOf(AiPrompt::class, $prompt); $this->assertEquals(2, $prompt->getId()); $this->assertTrue($prompt->isFineTuningAvailable()); @@ -340,30 +342,31 @@ public function testClonePrompt(): void 'path' => '/ai/prompts/2/clones', 'method' => 'post', 'body' => json_encode($params), - 'response' => '{ - "data": { - "id": 2, - "name": "Pre-translate prompt", - "action": "pre_translate", - "aiProviderId": 2, - "aiModelId": "gpt-5.4", - "isEnabled": true, - "enabledProjectIds": [1], - "config": {}, - "promptPreview": null, - "isFineTuningAvailable": true, - "createdBy": 123, - "updatedBy": 456, - "lastUsedBy": 789, - "lastUsedAt": "2019-09-25T14:30:00+00:00", - "usageCount": 42, - "createdAt": "2019-09-20T11:11:05+00:00", - "updatedAt": "2019-09-20T12:22:20+00:00" - } - }', + 'response' => json_encode([ + 'data' => [ + 'id' => 2, + 'name' => 'Pre-translate prompt', + 'action' => 'pre_translate', + 'aiProviderId' => 2, + 'aiModelId' => 'gpt-5.4', + 'isEnabled' => true, + 'enabledProjectIds' => [1], + 'config' => [], + 'promptPreview' => null, + 'isFineTuningAvailable' => true, + 'createdBy' => 123, + 'updatedBy' => 456, + 'lastUsedBy' => 789, + 'lastUsedAt' => '2019-09-25T14:30:00+00:00', + 'usageCount' => 42, + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + ], + ]), ]); $cloned = $this->crowdin->ai->clonePrompt(2, $params); + $this->assertInstanceOf(AiPrompt::class, $cloned); $this->assertEquals(2, $cloned->getId()); $this->assertEquals('Pre-translate prompt', $cloned->getName()); @@ -383,20 +386,7 @@ public function testCreatePromptCompletion(): void 'path' => '/ai/prompts/2/completions', 'method' => 'post', 'body' => json_encode($params), - 'response' => '{ - "data": { - "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", - "status": "finished", - "progress": 100, - "attributes": { - "aiPromptId": 38 - }, - "createdAt": "2019-09-23T11:26:54+00:00", - "updatedAt": "2019-09-23T11:26:54+00:00", - "startedAt": "2019-09-23T11:26:54+00:00", - "finishedAt": "2019-09-23T11:26:54+00:00" - } - }', + 'response' => $this->promptCompletionResponse(), ]); $completion = $this->crowdin->ai->createPromptCompletion(2, $params); @@ -410,20 +400,7 @@ public function testGetPromptCompletion(): void $this->mockRequest([ 'path' => '/ai/prompts/2/completions/50fb3506-4127-4ba8-8296-f97dc7e3e0c3', 'method' => 'get', - 'response' => '{ - "data": { - "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", - "status": "finished", - "progress": 100, - "attributes": { - "aiPromptId": 38 - }, - "createdAt": "2019-09-23T11:26:54+00:00", - "updatedAt": "2019-09-23T11:26:54+00:00", - "startedAt": "2019-09-23T11:26:54+00:00", - "finishedAt": "2019-09-23T11:26:54+00:00" - } - }', + 'response' => $this->promptCompletionResponse(), ]); $completion = $this->crowdin->ai->getPromptCompletion(2, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); @@ -436,6 +413,24 @@ public function testGetPromptCompletion(): void $this->assertEquals('2019-09-23T11:26:54+00:00', $completion->getFinishedAt()); } + private function promptCompletionResponse(): string + { + return json_encode([ + 'data' => [ + 'identifier' => '50fb3506-4127-4ba8-8296-f97dc7e3e0c3', + 'status' => 'finished', + 'progress' => 100, + 'attributes' => [ + 'aiPromptId' => 38, + ], + 'createdAt' => '2019-09-23T11:26:54+00:00', + 'updatedAt' => '2019-09-23T11:26:54+00:00', + 'startedAt' => '2019-09-23T11:26:54+00:00', + 'finishedAt' => '2019-09-23T11:26:54+00:00', + ], + ]); + } + public function testDeletePromptCompletion(): void { $this->mockRequest([ @@ -452,15 +447,11 @@ public function testDownloadPromptCompletion(): void $this->mockRequest([ 'path' => '/ai/prompts/2/completions/50fb3506-4127-4ba8-8296-f97dc7e3e0c3/download', 'method' => 'get', - 'response' => '{ - "data": { - "url": "https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff", - "expireIn": "2019-09-20T10:31:21+00:00" - } - }', + 'response' => $this->downloadFileResponse(), ]); $download = $this->crowdin->ai->downloadPromptCompletion(2, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + $this->assertInstanceOf(DownloadFile::class, $download); $this->assertEquals('https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff', $download->getUrl()); $this->assertEquals('2019-09-20T10:31:21+00:00', $download->getExpireIn()); @@ -471,35 +462,41 @@ public function testListProviders(): void $this->mockRequest([ 'path' => '/ai/providers', 'method' => 'get', - 'response' => '{ - "data": [ - { - "data": { - "id": 2, - "name": "OpenAI", - "type": "open_ai", - "credentials": {"apiKey": "sk-..."}, - "config": { - "actionRules": [ - { - "action": "pre_translate", - "availableAiModelIds": ["gpt-5.4"] - } - ] - }, - "isEnabled": true, - "useSystemCredentials": false, - "createdAt": "2019-09-20T11:11:05+00:00", - "updatedAt": "2019-09-20T12:22:20+00:00", - "promptsCount": 42 - } - } - ], - "pagination": {"offset": 0, "limit": 25} - }', + 'response' => json_encode([ + 'data' => [ + [ + 'data' => [ + 'id' => 2, + 'name' => 'OpenAI', + 'type' => 'open_ai', + 'credentials' => [ + 'apiKey' => 'sk-...', + ], + 'config' => [ + 'actionRules' => [ + [ + 'action' => 'pre_translate', + 'availableAiModelIds' => ['gpt-5.4'], + ], + ], + ], + 'isEnabled' => true, + 'useSystemCredentials' => false, + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + 'promptsCount' => 42, + ], + ], + ], + 'pagination' => [ + 'offset' => 0, + 'limit' => 25, + ], + ]), ]); $providers = $this->crowdin->ai->listProviders(); + $this->assertInstanceOf(ModelCollection::class, $providers); $this->assertInstanceOf(AiProvider::class, $providers[0]); $this->assertEquals(2, $providers[0]->getId()); @@ -520,23 +517,26 @@ public function testCreateProvider(): void 'path' => '/ai/providers', 'method' => 'post', 'body' => json_encode($params), - 'response' => '{ - "data": { - "id": 2, - "name": "OpenAI", - "type": "open_ai", - "credentials": {"apiKey": "sk-..."}, - "config": {}, - "isEnabled": true, - "useSystemCredentials": false, - "createdAt": "2019-09-20T11:11:05+00:00", - "updatedAt": "2019-09-20T12:22:20+00:00", - "promptsCount": 42 - } - }', + 'response' => json_encode([ + 'data' => [ + 'id' => 2, + 'name' => 'OpenAI', + 'type' => 'open_ai', + 'credentials' => [ + 'apiKey' => 'sk-...', + ], + 'config' => [], + 'isEnabled' => true, + 'useSystemCredentials' => false, + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + 'promptsCount' => 42, + ], + ]), ]); $provider = $this->crowdin->ai->createProvider($params); + $this->assertInstanceOf(AiProvider::class, $provider); $this->assertEquals(2, $provider->getId()); $this->assertEquals('OpenAI', $provider->getName()); @@ -547,30 +547,33 @@ public function testGetProvider(): void $this->mockRequest([ 'path' => '/ai/providers/2', 'method' => 'get', - 'response' => '{ - "data": { - "id": 2, - "name": "OpenAI", - "type": "open_ai", - "credentials": {"apiKey": "string"}, - "config": { - "actionRules": [ - { - "action": "pre_translate", - "availableAiModelIds": ["gpt-5.4"] - } - ] - }, - "isEnabled": true, - "useSystemCredentials": false, - "createdAt": "2019-09-20T11:11:05+00:00", - "updatedAt": "2019-09-20T12:22:20+00:00", - "promptsCount": 42 - } - }', + 'response' => json_encode([ + 'data' => [ + 'id' => 2, + 'name' => 'OpenAI', + 'type' => 'open_ai', + 'credentials' => [ + 'apiKey' => 'string', + ], + 'config' => [ + 'actionRules' => [ + [ + 'action' => 'pre_translate', + 'availableAiModelIds' => ['gpt-5.4'], + ], + ], + ], + 'isEnabled' => true, + 'useSystemCredentials' => false, + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + 'promptsCount' => 42, + ], + ]), ]); $provider = $this->crowdin->ai->getProvider(2); + $this->assertInstanceOf(AiProvider::class, $provider); $this->assertEquals(2, $provider->getId()); $this->assertFalse($provider->isUseSystemCredentials()); @@ -598,22 +601,26 @@ public function testListProviderModels(): void $this->mockRequest([ 'path' => '/ai/providers/2/models', 'method' => 'get', - 'response' => '{ - "data": [ - { - "data": { - "id": "gpt-5.4", - "provider": "open_ai", - "providerName": "OpenAI", - "providerId": 1 - } - } - ], - "pagination": {"offset": 0, "limit": 25} - }', + 'response' => json_encode([ + 'data' => [ + [ + 'data' => [ + 'id' => 'gpt-5.4', + 'provider' => 'open_ai', + 'providerName' => 'OpenAI', + 'providerId' => 1, + ], + ], + ], + 'pagination' => [ + 'offset' => 0, + 'limit' => 25, + ], + ]), ]); $models = $this->crowdin->ai->listProviderModels(2); + $this->assertInstanceOf(ModelCollection::class, $models); $this->assertInstanceOf(AiProviderModel::class, $models[0]); $this->assertEquals('gpt-5.4', $models[0]->getId()); @@ -634,17 +641,25 @@ public function testCreateProviderChatCompletion(): void 'path' => '/ai/providers/2/chat/completions', 'method' => 'post', 'body' => json_encode($params), - 'response' => '{ - "data": { - "id": "chatcmpl-123", - "object": "chat.completion", - "model": "gpt-4o", - "choices": [{"message": {"role": "assistant", "content": "Hi!"}}] - } - }', + 'response' => json_encode([ + 'data' => [ + 'id' => 'chatcmpl-123', + 'object' => 'chat.completion', + 'model' => 'gpt-4o', + 'choices' => [ + [ + 'message' => [ + 'role' => 'assistant', + 'content' => 'Hi!', + ], + ], + ], + ], + ]), ]); $completion = $this->crowdin->ai->createProviderChatCompletion(2, $params); + $this->assertInstanceOf(AiProxyChatCompletion::class, $completion); } @@ -666,25 +681,11 @@ public function testGenerateReport(): void 'path' => '/ai/reports', 'method' => 'post', 'body' => json_encode($params), - 'response' => '{ - "data": { - "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", - "status": "finished", - "progress": 100, - "attributes": { - "format": "json", - "reportType": "tokens-usage-raw-data", - "schema": {} - }, - "createdAt": "2024-01-23T11:26:54+00:00", - "updatedAt": "2024-09-23T11:26:54+00:00", - "startedAt": "2024-05-23T11:26:54+00:00", - "finishedAt": "2024-05-23T11:26:54+00:00" - } - }', + 'response' => $this->reportResponse(), ]); $report = $this->crowdin->ai->generateReport($params); + $this->assertInstanceOf(AiReport::class, $report); $this->assertEquals('50fb3506-4127-4ba8-8296-f97dc7e3e0c3', $report->getIdentifier()); $this->assertEquals('finished', $report->getStatus()); @@ -695,25 +696,11 @@ public function testGetReport(): void $this->mockRequest([ 'path' => '/ai/reports/50fb3506-4127-4ba8-8296-f97dc7e3e0c3', 'method' => 'get', - 'response' => '{ - "data": { - "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", - "status": "finished", - "progress": 100, - "attributes": { - "format": "json", - "reportType": "tokens-usage-raw-data", - "schema": {} - }, - "createdAt": "2024-01-23T11:26:54+00:00", - "updatedAt": "2024-09-23T11:26:54+00:00", - "startedAt": "2024-05-23T11:26:54+00:00", - "finishedAt": "2024-05-23T11:26:54+00:00" - } - }', + 'response' => $this->reportResponse(), ]); $report = $this->crowdin->ai->getReport('50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + $this->assertInstanceOf(AiReport::class, $report); $this->assertEquals('finished', $report->getStatus()); $this->assertEquals(100, $report->getProgress()); @@ -724,20 +711,36 @@ public function testGetReport(): void $this->assertEquals('2024-05-23T11:26:54+00:00', $report->getFinishedAt()); } + private function reportResponse(): string + { + return json_encode([ + 'data' => [ + 'identifier' => '50fb3506-4127-4ba8-8296-f97dc7e3e0c3', + 'status' => 'finished', + 'progress' => 100, + 'attributes' => [ + 'format' => 'json', + 'reportType' => 'tokens-usage-raw-data', + 'schema' => [], + ], + 'createdAt' => '2024-01-23T11:26:54+00:00', + 'updatedAt' => '2024-09-23T11:26:54+00:00', + 'startedAt' => '2024-05-23T11:26:54+00:00', + 'finishedAt' => '2024-05-23T11:26:54+00:00', + ], + ]); + } + public function testDownloadReport(): void { $this->mockRequest([ 'path' => '/ai/reports/50fb3506-4127-4ba8-8296-f97dc7e3e0c3/download', 'method' => 'get', - 'response' => '{ - "data": { - "url": "https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff", - "expireIn": "2019-09-20T10:31:21+00:00" - } - }', + 'response' => $this->downloadFileResponse(), ]); $download = $this->crowdin->ai->downloadReport('50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + $this->assertInstanceOf(DownloadFile::class, $download); $this->assertEquals('https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff', $download->getUrl()); $this->assertEquals('2019-09-20T10:31:21+00:00', $download->getExpireIn()); @@ -818,17 +821,18 @@ public function testGetSettings(): void $this->mockRequest([ 'path' => '/ai/settings', 'method' => 'get', - 'response' => '{ - "data": { - "preTranslationAiPromptId": 2, - "editorSuggestionAiPromptId": 5, - "qaCheckActionAiPromptId": 8, - "contextReviewAiPromptId": 11 - } - }', + 'response' => json_encode([ + 'data' => [ + 'preTranslationAiPromptId' => 2, + 'editorSuggestionAiPromptId' => 5, + 'qaCheckActionAiPromptId' => 8, + 'contextReviewAiPromptId' => 11, + ], + ]), ]); $settings = $this->crowdin->ai->getSettings(); + $this->assertInstanceOf(AiSettings::class, $settings); $this->assertEquals(2, $settings->getPreTranslationAiPromptId()); $this->assertEquals(5, $settings->getEditorSuggestionAiPromptId()); @@ -841,24 +845,28 @@ public function testListSnippets(): void $this->mockRequest([ 'path' => '/ai/settings/snippets', 'method' => 'get', - 'response' => '{ - "data": [ - { - "data": { - "id": 2, - "description": "Product description", - "placeholder": "%custom:productDescription%", - "value": "The product is the professional consulting service that transform challenges into opportunities.", - "createdAt": "2019-09-20T11:11:05+00:00", - "updatedAt": "2019-09-20T12:22:20+00:00" - } - } - ], - "pagination": {"offset": 0, "limit": 25} - }', + 'response' => json_encode([ + 'data' => [ + [ + 'data' => [ + 'id' => 2, + 'description' => 'Product description', + 'placeholder' => '%custom:productDescription%', + 'value' => 'The product is the professional consulting service that transform challenges into opportunities.', + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + ], + ], + ], + 'pagination' => [ + 'offset' => 0, + 'limit' => 25, + ], + ]), ]); $snippets = $this->crowdin->ai->listSnippets(); + $this->assertInstanceOf(ModelCollection::class, $snippets); $this->assertInstanceOf(AiSnippet::class, $snippets[0]); $this->assertEquals(2, $snippets[0]->getId()); @@ -877,19 +885,11 @@ public function testCreateSnippet(): void 'path' => '/ai/settings/snippets', 'method' => 'post', 'body' => json_encode($params), - 'response' => '{ - "data": { - "id": 2, - "description": "Product description", - "placeholder": "%custom:productDescription%", - "value": "The product is the professional consulting service that transform challenges into opportunities.", - "createdAt": "2019-09-20T11:11:05+00:00", - "updatedAt": "2019-09-20T12:22:20+00:00" - } - }', + 'response' => $this->snippetResponse(), ]); $snippet = $this->crowdin->ai->createSnippet($params); + $this->assertInstanceOf(AiSnippet::class, $snippet); $this->assertEquals(2, $snippet->getId()); $this->assertEquals('Product description', $snippet->getDescription()); @@ -900,19 +900,11 @@ public function testGetSnippet(): void $this->mockRequest([ 'path' => '/ai/settings/snippets/2', 'method' => 'get', - 'response' => '{ - "data": { - "id": 2, - "description": "Product description", - "placeholder": "%custom:productDescription%", - "value": "The product is the professional consulting service that transform challenges into opportunities.", - "createdAt": "2019-09-20T11:11:05+00:00", - "updatedAt": "2019-09-20T12:22:20+00:00" - } - }', + 'response' => $this->snippetResponse(), ]); $snippet = $this->crowdin->ai->getSnippet(2); + $this->assertInstanceOf(AiSnippet::class, $snippet); $this->assertEquals( 'The product is the professional consulting service that transform challenges into opportunities.', @@ -922,6 +914,20 @@ public function testGetSnippet(): void $this->assertEquals('2019-09-20T12:22:20+00:00', $snippet->getUpdatedAt()); } + private function snippetResponse(): string + { + return json_encode([ + 'data' => [ + 'id' => 2, + 'description' => 'Product description', + 'placeholder' => '%custom:productDescription%', + 'value' => 'The product is the professional consulting service that transform challenges into opportunities.', + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + ], + ]); + } + public function testDeleteSnippet(): void { $this->mockRequest([ @@ -987,6 +993,7 @@ public function testUpdatePrompt(): void ); $updated = $this->crowdin->ai->updatePrompt($prompt); + $this->assertInstanceOf(AiPrompt::class, $updated); $this->assertEquals(2, $updated->getId()); $this->assertEquals('Updated prompt', $updated->getName()); @@ -1033,6 +1040,7 @@ public function testUpdateProvider(): void ); $updated = $this->crowdin->ai->updateProvider($provider); + $this->assertInstanceOf(AiProvider::class, $updated); $this->assertEquals(2, $updated->getId()); $this->assertEquals('Azure OpenAI', $updated->getName()); @@ -1064,6 +1072,7 @@ public function testUpdateSettings(): void ); $updated = $this->crowdin->ai->updateSettings($settings); + $this->assertInstanceOf(AiSettings::class, $updated); $this->assertEquals(3, $updated->getPreTranslationAiPromptId()); $this->assertEquals(6, $updated->getEditorSuggestionAiPromptId()); @@ -1100,9 +1109,486 @@ public function testUpdateSnippet(): void ); $updated = $this->crowdin->ai->updateSnippet($snippet); + $this->assertInstanceOf(AiSnippet::class, $updated); $this->assertEquals('Updated description', $updated->getDescription()); $this->assertEquals('%custom:updatedDescription%', $updated->getPlaceholder()); $this->assertEquals('Updated value.', $updated->getValue()); } + + public function testGetProjectSettings(): void + { + $this->mockRequest([ + 'path' => '/projects/2/ai/settings', + 'method' => 'get', + 'response' => json_encode([ + 'data' => [ + 'editorSuggestionAiPromptId' => 6, + 'alignmentActionAiPromptId' => null, + 'qaCheckActionAiPromptId' => 9, + 'contextReviewAiPromptId' => 12, + ], + ]), + ]); + + $result = $this->crowdin->ai->getProjectSettings(2); + + $this->assertInstanceOf(ProjectAiSettings::class, $result); + $this->assertEquals(6, $result->getEditorSuggestionAiPromptId()); + $this->assertNull($result->getAlignmentActionAiPromptId()); + } + + public function testGenerateFineTuningDataset(): void + { + $data = [ + 'projectIds' => [1], + 'tmIds' => [2], + 'purpose' => 'training', + ]; + + $this->mockRequest([ + 'path' => '/ai/prompts/3/fine-tuning/datasets', + 'method' => 'post', + 'body' => json_encode($data), + 'response' => $this->fineTuningDatasetResponse(), + ]); + + $result = $this->crowdin->ai->generateFineTuningDataset(3, $data); + + $this->assertInstanceOf(AiFineTuningDataset::class, $result); + $this->assertEquals('1d5b5a0b-6b1c-4f1a-9f3b-2b1e1c3d4e5f', $result->getIdentifier()); + $this->assertEquals('training', $result->getAttributes()['purpose']); + } + + public function testGetFineTuningDataset(): void + { + $this->mockRequest([ + 'path' => '/ai/prompts/3/fine-tuning/datasets/1d5b5a0b-6b1c-4f1a-9f3b-2b1e1c3d4e5f', + 'method' => 'get', + 'response' => $this->fineTuningDatasetResponse(), + ]); + + $result = $this->crowdin->ai->getFineTuningDataset(3, '1d5b5a0b-6b1c-4f1a-9f3b-2b1e1c3d4e5f'); + + $this->assertInstanceOf(AiFineTuningDataset::class, $result); + $this->assertEquals('finished', $result->getStatus()); + } + + private function fineTuningDatasetResponse(): string + { + return json_encode([ + 'data' => [ + 'identifier' => '1d5b5a0b-6b1c-4f1a-9f3b-2b1e1c3d4e5f', + 'status' => 'finished', + 'progress' => 100, + 'attributes' => [ + 'projectIds' => [1], + 'tmIds' => [2], + 'purpose' => 'training', + ], + 'createdAt' => '2025-09-23T11:26:54+00:00', + 'updatedAt' => '2025-09-23T11:26:54+00:00', + 'startedAt' => '2025-09-23T11:26:54+00:00', + 'finishedAt' => '2025-09-23T11:26:54+00:00', + ], + ]); + } + + public function testDownloadFineTuningDataset(): void + { + $this->mockRequest([ + 'path' => '/ai/prompts/3/fine-tuning/datasets/1d5b5a0b-6b1c-4f1a-9f3b-2b1e1c3d4e5f/download', + 'method' => 'get', + 'response' => $this->jsonlDownloadResponse(), + ]); + + $result = $this->crowdin->ai->downloadFineTuningDataset(3, '1d5b5a0b-6b1c-4f1a-9f3b-2b1e1c3d4e5f'); + + $this->assertInstanceOf(DownloadFile::class, $result); + $this->assertEquals('https://production-enterprise-importer.downloads.crowdin.com/file.jsonl', $result->getUrl()); + } + + private function jsonlDownloadResponse(): string + { + return json_encode([ + 'data' => [ + 'url' => 'https://production-enterprise-importer.downloads.crowdin.com/file.jsonl', + 'expireIn' => '2025-09-20T10:31:21+00:00', + ], + ]); + } + + public function testListFineTuningJobs(): void + { + $this->mockRequest([ + 'path' => '/ai/prompts/fine-tuning/jobs?statuses=in_progress', + 'method' => 'get', + 'response' => json_encode([ + 'data' => [ + [ + 'data' => [ + 'identifier' => 'a1b2c3d4-0000-4000-8000-000000000001', + 'status' => 'in_progress', + 'progress' => 50, + 'attributes' => [ + 'dryRun' => false, + 'aiPromptId' => 3, + 'trainingOptions' => [ + 'projectIds' => [1], + ], + 'fineTunedModel' => null, + ], + 'createdAt' => '2025-09-23T11:26:54+00:00', + 'updatedAt' => '2025-09-23T11:26:54+00:00', + 'startedAt' => '2025-09-23T11:26:54+00:00', + 'finishedAt' => null, + ], + ], + ], + 'pagination' => [ + 'offset' => 0, + 'limit' => 25, + ], + ]), + ]); + + $result = $this->crowdin->ai->listFineTuningJobs(['statuses' => 'in_progress']); + + $this->assertInstanceOf(ModelCollection::class, $result); + $this->assertCount(1, $result); + $this->assertInstanceOf(AiFineTuningJob::class, $result[0]); + $this->assertEquals('in_progress', $result[0]->getStatus()); + } + + public function testCreateFineTuningJob(): void + { + $data = [ + 'dryRun' => false, + 'hyperparameters' => [ + 'nEpochs' => 3, + ], + 'trainingOptions' => [ + 'projectIds' => [1], + ], + ]; + + $this->mockRequest([ + 'path' => '/ai/prompts/3/fine-tuning/jobs', + 'method' => 'post', + 'body' => json_encode($data), + 'response' => $this->fineTuningJobResponse(), + ]); + + $result = $this->crowdin->ai->createFineTuningJob(3, $data); + + $this->assertInstanceOf(AiFineTuningJob::class, $result); + $this->assertEquals(3, $result->getAttributes()['aiPromptId']); + } + + public function testGetFineTuningJob(): void + { + $this->mockRequest([ + 'path' => '/ai/prompts/3/fine-tuning/jobs/a1b2c3d4-0000-4000-8000-000000000001', + 'method' => 'get', + 'response' => $this->fineTuningJobResponse(), + ]); + + $result = $this->crowdin->ai->getFineTuningJob(3, 'a1b2c3d4-0000-4000-8000-000000000001'); + + $this->assertInstanceOf(AiFineTuningJob::class, $result); + $this->assertEquals(50, $result->getProgress()); + $this->assertNull($result->getFinishedAt()); + } + + private function fineTuningJobResponse(): string + { + return json_encode([ + 'data' => [ + 'identifier' => 'a1b2c3d4-0000-4000-8000-000000000001', + 'status' => 'in_progress', + 'progress' => 50, + 'attributes' => [ + 'dryRun' => false, + 'aiPromptId' => 3, + 'trainingOptions' => [ + 'projectIds' => [1], + ], + 'fineTunedModel' => null, + ], + 'createdAt' => '2025-09-23T11:26:54+00:00', + 'updatedAt' => '2025-09-23T11:26:54+00:00', + 'startedAt' => '2025-09-23T11:26:54+00:00', + 'finishedAt' => null, + ], + ]); + } + + public function testListFineTuningEvents(): void + { + $this->mockRequest([ + 'path' => '/ai/prompts/3/fine-tuning/jobs/a1b2c3d4-0000-4000-8000-000000000001/events', + 'method' => 'get', + 'response' => json_encode([ + 'data' => [ + [ + 'data' => [ + 'id' => 'ftevent-1', + 'type' => 'metrics', + 'message' => 'Step 1/10', + 'data' => [ + 'step' => 1, + 'totalSteps' => 10, + 'trainingLoss' => 0.5, + ], + 'createdAt' => '2025-09-23T11:26:54+00:00', + ], + ], + ], + 'pagination' => [ + 'offset' => 0, + 'limit' => 25, + ], + ]), + ]); + + $result = $this->crowdin->ai->listFineTuningEvents(3, 'a1b2c3d4-0000-4000-8000-000000000001'); + + $this->assertInstanceOf(ModelCollection::class, $result); + $this->assertCount(1, $result); + $this->assertInstanceOf(AiFineTuningEvent::class, $result[0]); + $this->assertEquals('metrics', $result[0]->getType()); + $this->assertEquals(10, $result[0]->getEventData()['totalSteps']); + } + + public function testListAllProviderModels(): void + { + $this->mockRequest([ + 'path' => '/ai/providers/models', + 'method' => 'get', + 'response' => json_encode([ + 'data' => [ + [ + 'data' => [ + 'id' => 'gpt-4.1', + 'provider' => 'open_ai', + 'providerName' => 'OpenAI', + 'providerId' => 2, + 'contextWindow' => 128000, + 'maxOutputTokens' => 16384, + 'supportsStreaming' => true, + 'supportsFunctionCalling' => true, + 'supportsJsonMode' => true, + 'supportsJsonSchema' => true, + 'supportsVision' => false, + 'isCompatibleWithAiLimit' => true, + ], + ], + ], + 'pagination' => [ + 'offset' => 0, + 'limit' => 25, + ], + ]), + ]); + + $result = $this->crowdin->ai->listAllProviderModels(); + + $this->assertInstanceOf(ModelCollection::class, $result); + $this->assertCount(1, $result); + $this->assertInstanceOf(AiProviderModel::class, $result[0]); + $this->assertEquals(128000, $result[0]->getContextWindow()); + $this->assertTrue($result[0]->getIsCompatibleWithAiLimit()); + } + + public function testListSupportedProviderModels(): void + { + $this->mockRequest([ + 'path' => '/ai/providers/supported-models?providerType=open_ai', + 'method' => 'get', + 'response' => json_encode([ + 'data' => [ + [ + 'data' => [ + 'providerId' => 2, + 'providerType' => 'open_ai', + 'providerName' => 'OpenAI', + 'id' => 'gpt-4.1', + 'displayName' => 'GPT-4.1', + 'supportReasoning' => false, + 'intelligence' => 4, + 'speed' => 3, + 'price' => [ + 'input' => 2.0, + 'output' => 8.0, + ], + 'modalities' => [ + 'input' => ['text'], + 'output' => ['text'], + ], + 'contextWindow' => 128000, + 'maxOutputTokens' => 16384, + 'knowledgeCutoff' => '2024-06', + 'releaseDate' => '2025-04-14', + 'features' => [ + 'streaming' => true, + 'structuredOutput' => true, + 'functionCalling' => true, + ], + ], + ], + ], + 'pagination' => [ + 'offset' => 0, + 'limit' => 25, + ], + ]), + ]); + + $result = $this->crowdin->ai->listSupportedProviderModels(['providerType' => 'open_ai']); + + $this->assertInstanceOf(ModelCollection::class, $result); + $this->assertCount(1, $result); + $this->assertInstanceOf(AiSupportedModel::class, $result[0]); + $this->assertEquals('GPT-4.1', $result[0]->getDisplayName()); + } + + public function testExportRequestLogs(): void + { + $data = [ + 'format' => 'csv', + 'projectId' => 1, + ]; + + $this->mockRequest([ + 'path' => '/ai/request-logs/exports', + 'method' => 'post', + 'body' => json_encode($data), + 'response' => $this->requestLogExportResponse(), + ]); + + $result = $this->crowdin->ai->exportRequestLogs($data); + + $this->assertInstanceOf(AiRequestLogExport::class, $result); + $this->assertEquals('created', $result->getStatus()); + $this->assertEquals('10 seconds', $result->getEta()); + } + + public function testGetRequestLogsExport(): void + { + $this->mockRequest([ + 'path' => '/ai/request-logs/exports/e7c1d7f2-1111-4222-8333-444455556666', + 'method' => 'get', + 'response' => $this->requestLogExportResponse(), + ]); + + $result = $this->crowdin->ai->getRequestLogsExport('e7c1d7f2-1111-4222-8333-444455556666'); + + $this->assertInstanceOf(AiRequestLogExport::class, $result); + $this->assertEquals('e7c1d7f2-1111-4222-8333-444455556666', $result->getIdentifier()); + } + + private function requestLogExportResponse(): string + { + return json_encode([ + 'data' => [ + 'identifier' => 'e7c1d7f2-1111-4222-8333-444455556666', + 'status' => 'created', + 'progress' => 0, + 'attributes' => [ + 'format' => 'csv', + 'filters' => [ + 'projectId' => 1, + ], + ], + 'createdAt' => '2025-09-23T11:26:54+00:00', + 'updatedAt' => '2025-09-23T11:26:54+00:00', + 'startedAt' => null, + 'finishedAt' => null, + 'eta' => '10 seconds', + ], + ]); + } + + public function testDownloadRequestLogsExport(): void + { + $this->mockRequest([ + 'path' => '/ai/request-logs/exports/e7c1d7f2-1111-4222-8333-444455556666/download', + 'method' => 'get', + 'response' => $this->jsonlDownloadResponse(), + ]); + + $result = $this->crowdin->ai->downloadRequestLogsExport('e7c1d7f2-1111-4222-8333-444455556666'); + + $this->assertInstanceOf(DownloadFile::class, $result); + $this->assertEquals('2025-09-20T10:31:21+00:00', $result->getExpireIn()); + } + + public function testListUsageMembers(): void + { + $this->mockRequest([ + 'path' => '/ai/usage/members?limit=10', + 'method' => 'get', + 'response' => json_encode([ + 'data' => [ + [ + 'data' => [ + 'user' => [ + 'id' => 12, + 'username' => 'john', + 'fullName' => 'John Smith', + 'avatarUrl' => '', + ], + 'dailyCostLimit' => 5, + 'dailyCostSpent' => 1.25, + 'dailyResetAt' => '2025-09-24T00:00:00+00:00', + 'monthlyCostLimit' => null, + 'monthlyCostSpent' => 10.5, + 'monthlyResetAt' => '2025-10-01T00:00:00+00:00', + ], + ], + ], + 'pagination' => [ + 'offset' => 0, + 'limit' => 25, + ], + ]), + ]); + + $result = $this->crowdin->ai->listUsageMembers(['limit' => 10]); + + $this->assertInstanceOf(ModelCollection::class, $result); + $this->assertCount(1, $result); + $this->assertInstanceOf(AiMemberUsage::class, $result[0]); + $this->assertEquals(12, $result[0]->getUser()['id']); + } + + public function testGetUsageMember(): void + { + $this->mockRequest([ + 'path' => '/ai/usage/members/12', + 'method' => 'get', + 'response' => json_encode([ + 'data' => [ + 'user' => [ + 'id' => 12, + 'username' => 'john', + 'fullName' => 'John Smith', + 'avatarUrl' => '', + ], + 'dailyCostLimit' => 5, + 'dailyCostSpent' => 1.25, + 'dailyResetAt' => '2025-09-24T00:00:00+00:00', + 'monthlyCostLimit' => null, + 'monthlyCostSpent' => 10.5, + 'monthlyResetAt' => '2025-10-01T00:00:00+00:00', + ], + ]), + ]); + + $result = $this->crowdin->ai->getUsageMember(12); + + $this->assertInstanceOf(AiMemberUsage::class, $result); + $this->assertEquals(5.0, $result->getDailyCostLimit()); + $this->assertNull($result->getMonthlyCostLimit()); + } } diff --git a/tests/CrowdinApiClient/Api/TranslationMemoryApiTest.php b/tests/CrowdinApiClient/Api/TranslationMemoryApiTest.php index 8a96cfda..d655fe38 100644 --- a/tests/CrowdinApiClient/Api/TranslationMemoryApiTest.php +++ b/tests/CrowdinApiClient/Api/TranslationMemoryApiTest.php @@ -570,7 +570,23 @@ public function testUpdateSegment(): void 'path' => '/tms/4/segments/1', 'method' => 'patch', 'body' => json_encode($data), - 'response' => $this->segmentResponse(), + 'response' => json_encode([ + 'data' => [ + 'id' => 1, + 'records' => [ + [ + 'id' => 7, + 'languageId' => 'uk', + 'text' => 'Перекладений текст', + 'usageCount' => 0, + 'createdBy' => 1, + 'updatedBy' => 1, + 'createdAt' => '2023-09-20T11:34:40+00:00', + 'updatedAt' => '2023-09-20T11:34:40+00:00', + ], + ], + ], + ]), ]); $segment = $this->crowdin->translationMemory->updateSegment(4, 1, $data); @@ -579,65 +595,4 @@ public function testUpdateSegment(): void $this->assertEquals(1, $segment->getId()); $this->assertEquals(7, $segment->getRecords()[0]->getId()); } - - public function testCreateSegmentRecords(): void - { - $data = ['records' => [['languageId' => 'uk', 'text' => 'Перекладений текст']]]; - - $this->mockRequest([ - 'path' => '/tms/4/segments/1/records', - 'method' => 'post', - 'body' => json_encode($data), - 'response' => $this->segmentResponse(), - ]); - - $segment = $this->crowdin->translationMemory->createSegmentRecords(4, 1, $data); - - $this->assertInstanceOf(TranslationMemorySegment::class, $segment); - $this->assertEquals('uk', $segment->getRecords()[0]->getLanguageId()); - } - - public function testUpdateSegmentRecord(): void - { - $data = [['op' => 'replace', 'path' => '/text', 'value' => 'Перекладений текст']]; - - $this->mockRequest([ - 'path' => '/tms/4/segments/1/records/7', - 'method' => 'patch', - 'body' => json_encode($data), - 'response' => $this->segmentResponse(), - ]); - - $segment = $this->crowdin->translationMemory->updateSegmentRecord(4, 1, 7, $data); - - $this->assertInstanceOf(TranslationMemorySegment::class, $segment); - $this->assertEquals('Перекладений текст', $segment->getRecords()[0]->getText()); - } - - private function segmentResponse(): string - { - return json_encode([ - 'data' => [ - 'id' => 1, - 'records' => [ - [ - 'id' => 7, - 'languageId' => 'uk', - 'text' => 'Перекладений текст', - 'usageCount' => 0, - 'createdBy' => 1, - 'updatedBy' => 1, - 'createdAt' => '2023-09-20T11:34:40+00:00', - 'updatedAt' => '2023-09-20T11:34:40+00:00', - ], - ], - ], - ]); - } - - public function testDeleteSegmentRecord(): void - { - $this->mockRequestDelete('/tms/4/segments/1/records/7'); - $this->crowdin->translationMemory->deleteSegmentRecord(4, 1, 7); - } } diff --git a/tests/CrowdinApiClient/Model/AiFineTuningEventTest.php b/tests/CrowdinApiClient/Model/AiFineTuningEventTest.php new file mode 100644 index 00000000..fb0cf572 --- /dev/null +++ b/tests/CrowdinApiClient/Model/AiFineTuningEventTest.php @@ -0,0 +1,37 @@ + 'ftevent-1', + 'type' => 'metrics', + 'message' => 'Step 1/10', + 'data' => ['step' => 1, 'totalSteps' => 10, 'trainingLoss' => 0.5], + 'createdAt' => '2025-09-23T11:26:54+00:00', + ]; + $event = new AiFineTuningEvent($data); + + $this->assertSame('ftevent-1', $event->getId()); + $this->assertSame('metrics', $event->getType()); + $this->assertSame('Step 1/10', $event->getMessage()); + $this->assertSame($data['data'], $event->getEventData()); + $this->assertSame('2025-09-23T11:26:54+00:00', $event->getCreatedAt()); + $this->assertSame($data, $event->getData(), 'BaseModel raw payload must stay intact'); + } + + public function testLoadDataWithoutEventData(): void + { + $event = new AiFineTuningEvent(['id' => 'ftevent-2', 'type' => 'message', 'message' => 'Started']); + + $this->assertSame([], $event->getEventData()); + } +} diff --git a/tests/CrowdinApiClient/Model/AiMemberUsageTest.php b/tests/CrowdinApiClient/Model/AiMemberUsageTest.php new file mode 100644 index 00000000..d592b277 --- /dev/null +++ b/tests/CrowdinApiClient/Model/AiMemberUsageTest.php @@ -0,0 +1,47 @@ + 12, 'username' => 'john', 'fullName' => 'John Smith', 'avatarUrl' => '']; + $usage = new AiMemberUsage([ + 'user' => $user, + 'dailyCostLimit' => 5, + 'dailyCostSpent' => 1.25, + 'dailyResetAt' => '2025-09-24T00:00:00+00:00', + 'monthlyCostLimit' => 100.5, + 'monthlyCostSpent' => 10, + 'monthlyResetAt' => '2025-10-01T00:00:00+00:00', + ]); + + $this->assertSame($user, $usage->getUser()); + $this->assertSame(5.0, $usage->getDailyCostLimit()); + $this->assertSame(1.25, $usage->getDailyCostSpent()); + $this->assertSame('2025-09-24T00:00:00+00:00', $usage->getDailyResetAt()); + $this->assertSame(100.5, $usage->getMonthlyCostLimit()); + $this->assertSame(10.0, $usage->getMonthlyCostSpent()); + $this->assertSame('2025-10-01T00:00:00+00:00', $usage->getMonthlyResetAt()); + } + + public function testLoadDataWithoutLimits(): void + { + $usage = new AiMemberUsage([ + 'user' => ['id' => 12], + 'dailyCostLimit' => null, + 'dailyCostSpent' => 0, + 'monthlyCostLimit' => null, + 'monthlyCostSpent' => 0, + ]); + + $this->assertNull($usage->getDailyCostLimit()); + $this->assertNull($usage->getMonthlyCostLimit()); + } +} diff --git a/tests/CrowdinApiClient/Model/AiProviderModelTest.php b/tests/CrowdinApiClient/Model/AiProviderModelTest.php new file mode 100644 index 00000000..831588ba --- /dev/null +++ b/tests/CrowdinApiClient/Model/AiProviderModelTest.php @@ -0,0 +1,55 @@ + 'gpt-4.1', + 'provider' => 'open_ai', + 'providerName' => 'OpenAI', + 'providerId' => 2, + 'contextWindow' => 128000, + 'maxOutputTokens' => 16384, + 'supportsStreaming' => true, + 'supportsFunctionCalling' => true, + 'supportsJsonMode' => false, + 'supportsJsonSchema' => true, + 'supportsVision' => false, + 'isCompatibleWithAiLimit' => true, + ]); + + $this->assertSame('gpt-4.1', $model->getId()); + $this->assertSame('open_ai', $model->getProvider()); + $this->assertSame('OpenAI', $model->getProviderName()); + $this->assertSame(2, $model->getProviderId()); + $this->assertSame(128000, $model->getContextWindow()); + $this->assertSame(16384, $model->getMaxOutputTokens()); + $this->assertTrue($model->getSupportsStreaming()); + $this->assertTrue($model->getSupportsFunctionCalling()); + $this->assertFalse($model->getSupportsJsonMode()); + $this->assertTrue($model->getSupportsJsonSchema()); + $this->assertFalse($model->getSupportsVision()); + $this->assertTrue($model->getIsCompatibleWithAiLimit()); + } + + public function testLoadDataWithoutOptionalFields(): void + { + $model = new AiProviderModel(['id' => 'gpt-4.1']); + + $this->assertNull($model->getProvider()); + $this->assertNull($model->getProviderId()); + $this->assertNull($model->getContextWindow()); + $this->assertNull($model->getMaxOutputTokens()); + $this->assertNull($model->getSupportsStreaming()); + $this->assertNull($model->getSupportsVision()); + $this->assertFalse($model->getIsCompatibleWithAiLimit()); + } +} diff --git a/tests/CrowdinApiClient/Model/AiRequestLogExportTest.php b/tests/CrowdinApiClient/Model/AiRequestLogExportTest.php new file mode 100644 index 00000000..96810fbd --- /dev/null +++ b/tests/CrowdinApiClient/Model/AiRequestLogExportTest.php @@ -0,0 +1,48 @@ + 'e7c1d7f2-1111-4222-8333-444455556666', + 'status' => 'finished', + 'progress' => 100, + 'attributes' => ['format' => 'csv', 'filters' => ['projectId' => 1]], + 'createdAt' => '2025-09-23T11:26:54+00:00', + 'updatedAt' => '2025-09-23T11:27:54+00:00', + 'startedAt' => '2025-09-23T11:26:55+00:00', + 'finishedAt' => '2025-09-23T11:27:54+00:00', + 'eta' => '0 seconds', + ]); + + $this->assertSame('e7c1d7f2-1111-4222-8333-444455556666', $export->getIdentifier()); + $this->assertSame('finished', $export->getStatus()); + $this->assertSame(100, $export->getProgress()); + $this->assertSame('csv', $export->getAttributes()['format']); + $this->assertSame('2025-09-23T11:27:54+00:00', $export->getFinishedAt()); + $this->assertSame('0 seconds', $export->getEta()); + } + + public function testLoadDataWithoutOptionalFields(): void + { + $export = new AiRequestLogExport([ + 'identifier' => 'e7c1d7f2-1111-4222-8333-444455556666', + 'status' => 'created', + 'progress' => 0, + 'attributes' => ['format' => 'csv', 'filters' => []], + 'createdAt' => '2025-09-23T11:26:54+00:00', + ]); + + $this->assertNull($export->getStartedAt()); + $this->assertNull($export->getFinishedAt()); + $this->assertNull($export->getEta()); + } +} diff --git a/tests/CrowdinApiClient/Model/AiSupportedModelTest.php b/tests/CrowdinApiClient/Model/AiSupportedModelTest.php new file mode 100644 index 00000000..efedc7be --- /dev/null +++ b/tests/CrowdinApiClient/Model/AiSupportedModelTest.php @@ -0,0 +1,58 @@ + 2, + 'providerType' => 'open_ai', + 'providerName' => 'OpenAI', + 'id' => 'gpt-4.1', + 'displayName' => 'GPT-4.1', + 'supportReasoning' => false, + 'intelligence' => 4, + 'speed' => 3, + 'price' => ['input' => 2.0, 'output' => 8.0], + 'modalities' => ['input' => ['text'], 'output' => ['text']], + 'contextWindow' => 128000, + 'maxOutputTokens' => 16384, + 'knowledgeCutoff' => '2024-06', + 'releaseDate' => '2025-04-14', + 'features' => ['streaming' => true, 'structuredOutput' => true, 'functionCalling' => true], + ]; + $model = new AiSupportedModel($data); + + $this->assertSame(2, $model->getProviderId()); + $this->assertSame('open_ai', $model->getProviderType()); + $this->assertSame('OpenAI', $model->getProviderName()); + $this->assertSame('gpt-4.1', $model->getId()); + $this->assertSame('GPT-4.1', $model->getDisplayName()); + $this->assertFalse($model->getSupportReasoning()); + $this->assertSame(4, $model->getIntelligence()); + $this->assertSame(3, $model->getSpeed()); + $this->assertSame($data['price'], $model->getPrice()); + $this->assertSame($data['modalities'], $model->getModalities()); + $this->assertSame(128000, $model->getContextWindow()); + $this->assertSame(16384, $model->getMaxOutputTokens()); + $this->assertSame('2024-06', $model->getKnowledgeCutoff()); + $this->assertSame('2025-04-14', $model->getReleaseDate()); + $this->assertSame($data['features'], $model->getFeatures()); + } + + public function testLoadDataWithoutOptionalFields(): void + { + $model = new AiSupportedModel(['id' => 'gpt-4.1', 'providerType' => 'open_ai']); + + $this->assertNull($model->getProviderId()); + $this->assertNull($model->getKnowledgeCutoff()); + $this->assertNull($model->getReleaseDate()); + } +} diff --git a/tests/CrowdinApiClient/Model/ProjectAiSettingsTest.php b/tests/CrowdinApiClient/Model/ProjectAiSettingsTest.php new file mode 100644 index 00000000..4d20cbf6 --- /dev/null +++ b/tests/CrowdinApiClient/Model/ProjectAiSettingsTest.php @@ -0,0 +1,36 @@ + 6, + 'alignmentActionAiPromptId' => 7, + 'qaCheckActionAiPromptId' => '9', + 'contextReviewAiPromptId' => 12, + ]); + + $this->assertSame(6, $settings->getEditorSuggestionAiPromptId()); + $this->assertSame(7, $settings->getAlignmentActionAiPromptId()); + $this->assertSame(9, $settings->getQaCheckActionAiPromptId()); + $this->assertSame(12, $settings->getContextReviewAiPromptId()); + } + + public function testLoadDataWithoutOptionalFields(): void + { + $settings = new ProjectAiSettings([]); + + $this->assertNull($settings->getEditorSuggestionAiPromptId()); + $this->assertNull($settings->getAlignmentActionAiPromptId()); + $this->assertNull($settings->getQaCheckActionAiPromptId()); + $this->assertNull($settings->getContextReviewAiPromptId()); + } +} From c8e6da943970cec7130bf44152e1dc8c86dc2a9c Mon Sep 17 00:00:00 2001 From: Bohdan Bobko <172895427+bhdnb@users.noreply.github.com> Date: Wed, 30 Sep 2026 17:57:25 +0300 Subject: [PATCH 5/9] feat: add application KV storage and installation bundles and updates --- src/CrowdinApiClient/Api/ApplicationApi.php | 155 ++++++++++++++++++ .../Model/ApplicationInstallation.php | 48 ++++++ .../Model/ApplicationInstallationUpdate.php | 128 +++++++++++++++ .../Model/ApplicationKvRecord.php | 71 ++++++++ src/CrowdinApiClient/Model/BaseModel.php | 8 + .../Api/ApplicationApiTest.php | 148 +++++++++++++++++ .../Model/ApplicationInstallationTest.php | 27 +++ .../ApplicationInstallationUpdateTest.php | 50 ++++++ .../Model/ApplicationKvRecordTest.php | 39 +++++ 9 files changed, 674 insertions(+) create mode 100644 src/CrowdinApiClient/Model/ApplicationInstallationUpdate.php create mode 100644 src/CrowdinApiClient/Model/ApplicationKvRecord.php create mode 100644 tests/CrowdinApiClient/Model/ApplicationInstallationUpdateTest.php create mode 100644 tests/CrowdinApiClient/Model/ApplicationKvRecordTest.php diff --git a/src/CrowdinApiClient/Api/ApplicationApi.php b/src/CrowdinApiClient/Api/ApplicationApi.php index 6e1dd2f3..a5146ff7 100644 --- a/src/CrowdinApiClient/Api/ApplicationApi.php +++ b/src/CrowdinApiClient/Api/ApplicationApi.php @@ -7,6 +7,8 @@ use CrowdinApiClient\Model\ApplicationConsent; use CrowdinApiClient\Model\ApplicationData; use CrowdinApiClient\Model\ApplicationInstallation; +use CrowdinApiClient\Model\ApplicationInstallationUpdate; +use CrowdinApiClient\Model\ApplicationKvRecord; use CrowdinApiClient\ModelCollection; /** @@ -225,4 +227,157 @@ public function editApplicationData(string $applicationIdentifier, string $path, $url = sprintf('applications/%s/api/%s', $applicationIdentifier, $path); return $this->_patch($url, ApplicationData::class, $data); } + + /** + * Upload Application Installation Bundle + * @link https://developer.crowdin.com/api/v2/#operation/api.applications.installations.bundles.post API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.applications.installations.bundles.post API Documentation Enterprise + * + * @param string $identifier + * @param array $data + * integer $data[storageId] required. ZIP archive with a non-empty app.js at its root + * @return ApplicationInstallation|null + */ + public function uploadInstallationBundle(string $identifier, array $data): ?ApplicationInstallation + { + return $this->_post( + sprintf('applications/installations/%s/bundles', $identifier), + ApplicationInstallation::class, + $data + ); + } + + /** + * Get Application Installation Update + * @link https://developer.crowdin.com/api/v2/#operation/api.applications.installations.update.get API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.applications.installations.update.get API Documentation Enterprise + * + * @param string $identifier + * @return ApplicationInstallationUpdate|null + */ + public function getInstallationUpdate(string $identifier): ?ApplicationInstallationUpdate + { + return $this->_get( + sprintf('applications/installations/%s/update', $identifier), + ApplicationInstallationUpdate::class + ); + } + + /** + * Apply Application Installation Update + * @link https://developer.crowdin.com/api/v2/#operation/api.applications.installations.update.post API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.applications.installations.update.post API Documentation Enterprise + * + * @param string $identifier + * @param array $data + * string $data[manifestHash] required. From getInstallationUpdate() + * @return ApplicationInstallation|null + */ + public function applyInstallationUpdate(string $identifier, array $data): ?ApplicationInstallation + { + return $this->_post( + sprintf('applications/installations/%s/update', $identifier), + ApplicationInstallation::class, + $data + ); + } + + /** + * List Application KV Records + * @link https://developer.crowdin.com/api/v2/#operation/api.applications.storage.kv.records.getMany API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.applications.storage.kv.records.getMany API Documentation Enterprise + * Note: requires the application's own access token, personal access tokens are not supported. + * + * @param string $applicationIdentifier + * @param array $params + * string $params[prefix]
+ * string $params[orderBy]
+ * integer $params[limit]
+ * integer $params[offset] + * @return ModelCollection + */ + public function listKvRecords(string $applicationIdentifier, array $params = []): ModelCollection + { + return $this->_list( + sprintf('applications/%s/storage/kv/records', $applicationIdentifier), + ApplicationKvRecord::class, + $params + ); + } + + /** + * Add Application KV Record + * @link https://developer.crowdin.com/api/v2/#operation/api.applications.storage.kv.records.post API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.applications.storage.kv.records.post API Documentation Enterprise + * Note: requires the application's own access token, personal access tokens are not supported. + * + * @param string $applicationIdentifier + * @param array $data + * string $data[key] required
+ * mixed $data[value] required. Any JSON value except null
+ * boolean $data[secret]
+ * integer $data[ttl] Time to live in seconds (60-31536000) + * @return ApplicationKvRecord|null + */ + public function addKvRecord(string $applicationIdentifier, array $data): ?ApplicationKvRecord + { + return $this->_create( + sprintf('applications/%s/storage/kv/records', $applicationIdentifier), + ApplicationKvRecord::class, + $data + ); + } + + /** + * Get Application KV Record + * @link https://developer.crowdin.com/api/v2/#operation/api.applications.storage.kv.records.get API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.applications.storage.kv.records.get API Documentation Enterprise + * Note: requires the application's own access token, personal access tokens are not supported. + * + * @param string $applicationIdentifier + * @param string $key + * @return ApplicationKvRecord|null + */ + public function getKvRecord(string $applicationIdentifier, string $key): ?ApplicationKvRecord + { + return $this->_get( + sprintf('applications/%s/storage/kv/records/%s', $applicationIdentifier, $key), + ApplicationKvRecord::class + ); + } + + /** + * Edit Application KV Record + * @link https://developer.crowdin.com/api/v2/#operation/api.applications.storage.kv.records.patch API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.applications.storage.kv.records.patch API Documentation Enterprise + * Note: requires the application's own access token, personal access tokens are not supported. + * + * @param string $applicationIdentifier + * @param string $key + * @param array $data JSON Patch array. Paths: /value, /ttl (null value removes the TTL) + * @return ApplicationKvRecord|null + */ + public function updateKvRecord(string $applicationIdentifier, string $key, array $data): ?ApplicationKvRecord + { + return $this->_patch( + sprintf('applications/%s/storage/kv/records/%s', $applicationIdentifier, $key), + ApplicationKvRecord::class, + $data + ); + } + + /** + * Delete Application KV Record + * @link https://developer.crowdin.com/api/v2/#operation/api.applications.storage.kv.records.delete API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.applications.storage.kv.records.delete API Documentation Enterprise + * Note: requires the application's own access token, personal access tokens are not supported. + * + * @param string $applicationIdentifier + * @param string $key + * @return mixed + */ + public function deleteKvRecord(string $applicationIdentifier, string $key) + { + return $this->_delete(sprintf('applications/%s/storage/kv/records/%s', $applicationIdentifier, $key)); + } } diff --git a/src/CrowdinApiClient/Model/ApplicationInstallation.php b/src/CrowdinApiClient/Model/ApplicationInstallation.php index d091dc9a..16d43d0a 100644 --- a/src/CrowdinApiClient/Model/ApplicationInstallation.php +++ b/src/CrowdinApiClient/Model/ApplicationInstallation.php @@ -51,6 +51,21 @@ class ApplicationInstallation extends BaseModel /** @var bool */ protected $stringBasedAvailable; + /** @var string|null */ + protected $logoUrl; + + /** @var array|null */ + protected $manifest; + + /** @var string|null */ + protected $manifestUpdatedAt; + + /** @var bool */ + protected $isManifestOutdated; + + /** @var array|null */ + protected $bundle; + public function __construct(array $data = []) { parent::__construct($data); @@ -70,6 +85,11 @@ public function __construct(array $data = []) $this->defaultPermissions = (array)$this->getDataProperty('defaultPermissions'); $this->limitReached = (bool)$this->getDataProperty('limitReached'); $this->stringBasedAvailable = (bool)$this->getDataProperty('stringBasedAvailable'); + $this->logoUrl = $this->nullableString('logoUrl'); + $this->manifest = $this->nullableArray('manifest'); + $this->manifestUpdatedAt = $this->nullableString('manifestUpdatedAt'); + $this->isManifestOutdated = (bool)$this->getDataProperty('isManifestOutdated'); + $this->bundle = $this->nullableArray('bundle'); } public function getIdentifier(): string @@ -162,4 +182,32 @@ public function setModules(array $modules): void { $this->modules = $modules; } + + public function getLogoUrl(): ?string + { + return $this->logoUrl; + } + + public function getManifest(): ?array + { + return $this->manifest; + } + + public function getManifestUpdatedAt(): ?string + { + return $this->manifestUpdatedAt; + } + + public function isManifestOutdated(): bool + { + return $this->isManifestOutdated; + } + + /** + * @return array|null mode ("internal" or "external") and, for external bundles, url + */ + public function getBundle(): ?array + { + return $this->bundle; + } } diff --git a/src/CrowdinApiClient/Model/ApplicationInstallationUpdate.php b/src/CrowdinApiClient/Model/ApplicationInstallationUpdate.php new file mode 100644 index 00000000..2f3195b1 --- /dev/null +++ b/src/CrowdinApiClient/Model/ApplicationInstallationUpdate.php @@ -0,0 +1,128 @@ +manifestHash = $this->nullableString('manifestHash'); + $this->latestManifest = $this->nullableArray('latestManifest'); + $this->addedScopes = (array)$this->getDataProperty('addedScopes'); + $this->removedScopes = (array)$this->getDataProperty('removedScopes'); + $this->addedModules = (array)$this->getDataProperty('addedModules'); + $this->removedModules = (array)$this->getDataProperty('removedModules'); + $this->changedModules = (array)$this->getDataProperty('changedModules'); + $this->changedEvents = (array)$this->getDataProperty('changedEvents'); + $this->baseUrlChanged = (array)$this->getDataProperty('baseUrlChanged'); + $this->authenticationTypeChanged = (array)$this->getDataProperty('authenticationTypeChanged'); + $this->hasChanges = (bool)$this->getDataProperty('hasChanges'); + } + + /** + * Pass to ApplicationApi::applyInstallationUpdate() as an optimistic-locking token. + */ + public function getManifestHash(): ?string + { + return $this->manifestHash; + } + + public function getLatestManifest(): ?array + { + return $this->latestManifest; + } + + public function getAddedScopes(): array + { + return $this->addedScopes; + } + + public function getRemovedScopes(): array + { + return $this->removedScopes; + } + + public function getAddedModules(): array + { + return $this->addedModules; + } + + public function getRemovedModules(): array + { + return $this->removedModules; + } + + public function getChangedModules(): array + { + return $this->changedModules; + } + + /** + * @return array event name => [from, to] + */ + public function getChangedEvents(): array + { + return $this->changedEvents; + } + + /** + * @return array from, to + */ + public function getBaseUrlChanged(): array + { + return $this->baseUrlChanged; + } + + /** + * @return array from, to + */ + public function getAuthenticationTypeChanged(): array + { + return $this->authenticationTypeChanged; + } + + public function hasChanges(): bool + { + return $this->hasChanges; + } +} diff --git a/src/CrowdinApiClient/Model/ApplicationKvRecord.php b/src/CrowdinApiClient/Model/ApplicationKvRecord.php new file mode 100644 index 00000000..ccf90ce8 --- /dev/null +++ b/src/CrowdinApiClient/Model/ApplicationKvRecord.php @@ -0,0 +1,71 @@ +key = (string)$this->getDataProperty('key'); + $this->value = $this->getDataProperty('value'); + $this->secret = (bool)$this->getDataProperty('secret'); + $this->createdAt = (string)$this->getDataProperty('createdAt'); + $this->updatedAt = (string)$this->getDataProperty('updatedAt'); + $this->expiresAt = $this->nullableString('expiresAt'); + } + + public function getKey(): string + { + return $this->key; + } + + /** + * @return mixed + */ + public function getValue() + { + return $this->value; + } + + public function isSecret(): bool + { + return $this->secret; + } + + public function getCreatedAt(): string + { + return $this->createdAt; + } + + public function getUpdatedAt(): string + { + return $this->updatedAt; + } + + public function getExpiresAt(): ?string + { + return $this->expiresAt; + } +} diff --git a/src/CrowdinApiClient/Model/BaseModel.php b/src/CrowdinApiClient/Model/BaseModel.php index a5acc3d9..ba8d645d 100644 --- a/src/CrowdinApiClient/Model/BaseModel.php +++ b/src/CrowdinApiClient/Model/BaseModel.php @@ -67,4 +67,12 @@ protected function nullableString(string $property): ?string { return $this->getDataProperty($property) !== null ? (string)$this->getDataProperty($property) : null; } + + /** + * Returns the property cast to array, or null when it is missing or null. + */ + protected function nullableArray(string $property): ?array + { + return $this->getDataProperty($property) !== null ? (array)$this->getDataProperty($property) : null; + } } diff --git a/tests/CrowdinApiClient/Api/ApplicationApiTest.php b/tests/CrowdinApiClient/Api/ApplicationApiTest.php index a904170d..cebaaca2 100644 --- a/tests/CrowdinApiClient/Api/ApplicationApiTest.php +++ b/tests/CrowdinApiClient/Api/ApplicationApiTest.php @@ -7,6 +7,8 @@ use CrowdinApiClient\Model\ApplicationConsent; use CrowdinApiClient\Model\ApplicationData; use CrowdinApiClient\Model\ApplicationInstallation; +use CrowdinApiClient\Model\ApplicationInstallationUpdate; +use CrowdinApiClient\Model\ApplicationKvRecord; use CrowdinApiClient\ModelCollection; class ApplicationApiTest extends AbstractTestApi @@ -326,4 +328,150 @@ public function testEditApplicationData(): void $this->assertInstanceOf(ApplicationData::class, $data); $this->assertEquals($this->getResponseData(), $data->getData()); } + + public function testUploadInstallationBundle(): void + { + $this->mockRequest([ + 'path' => '/applications/installations/my-app/bundles', + 'method' => 'post', + 'body' => json_encode(['storageId' => 5]), + 'response' => $this->getInstallationResponseJson(), + ]); + + $installation = $this->crowdin->application->uploadInstallationBundle('my-app', ['storageId' => 5]); + + $this->assertInstanceOf(ApplicationInstallation::class, $installation); + $this->assertEquals('my-app', $installation->getIdentifier()); + } + + public function testGetInstallationUpdate(): void + { + $this->mockRequestGet( + '/applications/installations/my-app/update', + json_encode([ + 'data' => [ + 'manifestHash' => 'a1b2c3', + 'latestManifest' => ['identifier' => 'my-app'], + 'addedScopes' => ['tm'], + 'removedScopes' => [], + 'addedModules' => [], + 'removedModules' => [], + 'changedModules' => [], + 'changedEvents' => [], + 'baseUrlChanged' => ['from' => null, 'to' => null], + 'authenticationTypeChanged' => ['from' => null, 'to' => null], + 'hasChanges' => true, + ], + ]) + ); + + $update = $this->crowdin->application->getInstallationUpdate('my-app'); + + $this->assertInstanceOf(ApplicationInstallationUpdate::class, $update); + $this->assertEquals('a1b2c3', $update->getManifestHash()); + $this->assertEquals(['tm'], $update->getAddedScopes()); + $this->assertTrue($update->hasChanges()); + } + + public function testApplyInstallationUpdate(): void + { + $this->mockRequest([ + 'path' => '/applications/installations/my-app/update', + 'method' => 'post', + 'body' => json_encode(['manifestHash' => 'a1b2c3']), + 'response' => $this->getInstallationResponseJson(), + ]); + + $installation = $this->crowdin->application->applyInstallationUpdate('my-app', ['manifestHash' => 'a1b2c3']); + + $this->assertInstanceOf(ApplicationInstallation::class, $installation); + } + + protected function getKvRecordData(): array + { + return [ + 'key' => 'settings.theme', + 'value' => ['color' => 'dark'], + 'secret' => false, + 'createdAt' => '2026-09-01T10:00:00+00:00', + 'updatedAt' => '2026-09-01T10:00:00+00:00', + 'expiresAt' => null, + ]; + } + + public function testListKvRecords(): void + { + $this->mockRequestGet( + '/applications/my-application/storage/kv/records?prefix=settings.', + json_encode([ + 'data' => [['data' => $this->getKvRecordData()]], + 'pagination' => ['offset' => 0, 'limit' => 25], + ]) + ); + + $records = $this->crowdin->application->listKvRecords($this->applicationIdentifier, ['prefix' => 'settings.']); + + $this->assertInstanceOf(ModelCollection::class, $records); + $this->assertCount(1, $records); + $this->assertInstanceOf(ApplicationKvRecord::class, $records[0]); + $this->assertEquals(['color' => 'dark'], $records[0]->getValue()); + } + + public function testAddKvRecord(): void + { + $data = ['key' => 'settings.theme', 'value' => ['color' => 'dark'], 'ttl' => 3600]; + + $this->mockRequest([ + 'path' => '/applications/my-application/storage/kv/records', + 'method' => 'post', + 'body' => json_encode($data), + 'response' => json_encode(['data' => $this->getKvRecordData()]), + ]); + + $record = $this->crowdin->application->addKvRecord($this->applicationIdentifier, $data); + + $this->assertInstanceOf(ApplicationKvRecord::class, $record); + $this->assertEquals('settings.theme', $record->getKey()); + } + + public function testGetKvRecord(): void + { + $this->mockRequestGet( + '/applications/my-application/storage/kv/records/settings.theme', + json_encode(['data' => $this->getKvRecordData()]) + ); + + $record = $this->crowdin->application->getKvRecord($this->applicationIdentifier, 'settings.theme'); + + $this->assertInstanceOf(ApplicationKvRecord::class, $record); + $this->assertFalse($record->isSecret()); + $this->assertNull($record->getExpiresAt()); + } + + public function testUpdateKvRecord(): void + { + $data = [ + ['op' => 'replace', 'path' => '/value', 'value' => ['color' => 'light']], + ['op' => 'replace', 'path' => '/ttl', 'value' => null], + ]; + + $this->mockRequest([ + 'path' => '/applications/my-application/storage/kv/records/settings.theme', + 'method' => 'patch', + 'body' => json_encode($data), + 'response' => json_encode(['data' => array_merge($this->getKvRecordData(), ['value' => ['color' => 'light']])]), + ]); + + $record = $this->crowdin->application->updateKvRecord($this->applicationIdentifier, 'settings.theme', $data); + + $this->assertInstanceOf(ApplicationKvRecord::class, $record); + $this->assertEquals(['color' => 'light'], $record->getValue()); + } + + public function testDeleteKvRecord(): void + { + $this->mockRequestDelete('/applications/my-application/storage/kv/records/settings.theme'); + + $this->crowdin->application->deleteKvRecord($this->applicationIdentifier, 'settings.theme'); + } } diff --git a/tests/CrowdinApiClient/Model/ApplicationInstallationTest.php b/tests/CrowdinApiClient/Model/ApplicationInstallationTest.php index 30651d07..6e0b1f34 100644 --- a/tests/CrowdinApiClient/Model/ApplicationInstallationTest.php +++ b/tests/CrowdinApiClient/Model/ApplicationInstallationTest.php @@ -123,4 +123,31 @@ public function testSetModules(): void $this->model->setModules($newModules); $this->assertEquals($newModules, $this->model->getModules()); } + + public function testManifestFieldsDefaultWhenAbsent(): void + { + $this->assertNull($this->model->getLogoUrl()); + $this->assertNull($this->model->getManifest()); + $this->assertNull($this->model->getManifestUpdatedAt()); + $this->assertFalse($this->model->isManifestOutdated()); + $this->assertNull($this->model->getBundle()); + } + + public function testManifestFields(): void + { + $model = new ApplicationInstallation([ + 'identifier' => 'my-app', + 'logoUrl' => 'https://localhost.dev/logo.png', + 'manifest' => ['identifier' => 'my-app', 'name' => 'My Application'], + 'manifestUpdatedAt' => '2026-09-01T10:00:00+00:00', + 'isManifestOutdated' => true, + 'bundle' => ['mode' => 'external', 'url' => 'https://localhost:3000'], + ]); + + $this->assertEquals('https://localhost.dev/logo.png', $model->getLogoUrl()); + $this->assertEquals(['identifier' => 'my-app', 'name' => 'My Application'], $model->getManifest()); + $this->assertEquals('2026-09-01T10:00:00+00:00', $model->getManifestUpdatedAt()); + $this->assertTrue($model->isManifestOutdated()); + $this->assertEquals(['mode' => 'external', 'url' => 'https://localhost:3000'], $model->getBundle()); + } } diff --git a/tests/CrowdinApiClient/Model/ApplicationInstallationUpdateTest.php b/tests/CrowdinApiClient/Model/ApplicationInstallationUpdateTest.php new file mode 100644 index 00000000..a970ed19 --- /dev/null +++ b/tests/CrowdinApiClient/Model/ApplicationInstallationUpdateTest.php @@ -0,0 +1,50 @@ + 'a1b2c3', + 'latestManifest' => ['identifier' => 'my-app'], + 'addedScopes' => ['tm'], + 'removedScopes' => ['glossary'], + 'addedModules' => [['key' => 'new-module']], + 'removedModules' => [['key' => 'old-module']], + 'changedModules' => [['key' => 'module-1']], + 'changedEvents' => ['installed' => ['from' => '/old', 'to' => '/new']], + 'baseUrlChanged' => ['from' => 'https://a.dev', 'to' => 'https://b.dev'], + 'authenticationTypeChanged' => ['from' => 'none', 'to' => 'crowdin_app'], + 'hasChanges' => true, + ]); + + $this->assertSame('a1b2c3', $update->getManifestHash()); + $this->assertSame(['identifier' => 'my-app'], $update->getLatestManifest()); + $this->assertSame(['tm'], $update->getAddedScopes()); + $this->assertSame(['glossary'], $update->getRemovedScopes()); + $this->assertSame([['key' => 'new-module']], $update->getAddedModules()); + $this->assertSame([['key' => 'old-module']], $update->getRemovedModules()); + $this->assertSame([['key' => 'module-1']], $update->getChangedModules()); + $this->assertSame(['installed' => ['from' => '/old', 'to' => '/new']], $update->getChangedEvents()); + $this->assertSame(['from' => 'https://a.dev', 'to' => 'https://b.dev'], $update->getBaseUrlChanged()); + $this->assertSame(['from' => 'none', 'to' => 'crowdin_app'], $update->getAuthenticationTypeChanged()); + $this->assertTrue($update->hasChanges()); + } + + public function testLoadDataWithoutChanges(): void + { + $update = new ApplicationInstallationUpdate(['hasChanges' => false]); + + $this->assertNull($update->getManifestHash()); + $this->assertNull($update->getLatestManifest()); + $this->assertSame([], $update->getAddedScopes()); + $this->assertFalse($update->hasChanges()); + } +} diff --git a/tests/CrowdinApiClient/Model/ApplicationKvRecordTest.php b/tests/CrowdinApiClient/Model/ApplicationKvRecordTest.php new file mode 100644 index 00000000..47de3e72 --- /dev/null +++ b/tests/CrowdinApiClient/Model/ApplicationKvRecordTest.php @@ -0,0 +1,39 @@ + 'user:1:token', + 'value' => ['color' => 'dark'], + 'secret' => true, + 'createdAt' => '2026-09-01T10:00:00+00:00', + 'updatedAt' => '2026-09-02T10:00:00+00:00', + 'expiresAt' => '2026-10-01T10:00:00+00:00', + ]); + + $this->assertSame('user:1:token', $record->getKey()); + $this->assertSame(['color' => 'dark'], $record->getValue()); + $this->assertTrue($record->isSecret()); + $this->assertSame('2026-09-01T10:00:00+00:00', $record->getCreatedAt()); + $this->assertSame('2026-09-02T10:00:00+00:00', $record->getUpdatedAt()); + $this->assertSame('2026-10-01T10:00:00+00:00', $record->getExpiresAt()); + } + + public function testScalarValueAndNoExpiry(): void + { + $record = new ApplicationKvRecord(['key' => 'counter', 'value' => 42]); + + $this->assertSame(42, $record->getValue()); + $this->assertFalse($record->isSecret()); + $this->assertNull($record->getExpiresAt()); + } +} From fa0e2c018aabb48965d3484b9915edb445ab42f0 Mon Sep 17 00:00:00 2001 From: Bohdan Bobko <172895427+bhdnb@users.noreply.github.com> Date: Wed, 30 Sep 2026 18:04:58 +0300 Subject: [PATCH 6/9] feat: add dictionaries, system placeholders and advisors APIs --- src/CrowdinApiClient/Api/AdvisorApi.php | 118 +++++++++++++ src/CrowdinApiClient/Api/DictionaryApi.php | 50 ++++++ .../Api/SystemPlaceholderApi.php | 59 +++++++ src/CrowdinApiClient/Crowdin.php | 9 + src/CrowdinApiClient/Model/AdvisorCheck.php | 89 ++++++++++ src/CrowdinApiClient/Model/AdvisorInsight.php | 160 ++++++++++++++++++ src/CrowdinApiClient/Model/Dictionary.php | 35 ++++ .../Model/SystemPlaceholder.php | 65 +++++++ tests/CrowdinApiClient/Api/AdvisorApiTest.php | 157 +++++++++++++++++ .../Api/DictionaryApiTest.php | 64 +++++++ .../Api/SystemPlaceholderApiTest.php | 74 ++++++++ .../Model/AdvisorCheckTest.php | 42 +++++ .../Model/AdvisorInsightTest.php | 67 ++++++++ .../CrowdinApiClient/Model/DictionaryTest.php | 24 +++ .../Model/SystemPlaceholderTest.php | 28 +++ 15 files changed, 1041 insertions(+) create mode 100644 src/CrowdinApiClient/Api/AdvisorApi.php create mode 100644 src/CrowdinApiClient/Api/DictionaryApi.php create mode 100644 src/CrowdinApiClient/Api/SystemPlaceholderApi.php create mode 100644 src/CrowdinApiClient/Model/AdvisorCheck.php create mode 100644 src/CrowdinApiClient/Model/AdvisorInsight.php create mode 100644 src/CrowdinApiClient/Model/Dictionary.php create mode 100644 src/CrowdinApiClient/Model/SystemPlaceholder.php create mode 100644 tests/CrowdinApiClient/Api/AdvisorApiTest.php create mode 100644 tests/CrowdinApiClient/Api/DictionaryApiTest.php create mode 100644 tests/CrowdinApiClient/Api/SystemPlaceholderApiTest.php create mode 100644 tests/CrowdinApiClient/Model/AdvisorCheckTest.php create mode 100644 tests/CrowdinApiClient/Model/AdvisorInsightTest.php create mode 100644 tests/CrowdinApiClient/Model/DictionaryTest.php create mode 100644 tests/CrowdinApiClient/Model/SystemPlaceholderTest.php diff --git a/src/CrowdinApiClient/Api/AdvisorApi.php b/src/CrowdinApiClient/Api/AdvisorApi.php new file mode 100644 index 00000000..c4f87ad0 --- /dev/null +++ b/src/CrowdinApiClient/Api/AdvisorApi.php @@ -0,0 +1,118 @@ + + * string $data[category] Can't be used with inspectors
+ * array $data[inspectors] Can't be used with category. List of key and options (mode: auto or all, promptId) + * @return AdvisorCheck|null + */ + public function createCheck(int $projectId, array $data = []): ?AdvisorCheck + { + return $this->_post(sprintf('projects/%d/advisors/checks', $projectId), AdvisorCheck::class, $data); + } + + /** + * Get Advisor Check Status + * @link https://developer.crowdin.com/api/v2/#operation/api.projects.advisors.checks.get API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.advisors.checks.get API Documentation Enterprise + * + * @param int $projectId + * @param string $checkId + * @return AdvisorCheck|null + */ + public function getCheck(int $projectId, string $checkId): ?AdvisorCheck + { + return $this->_get(sprintf('projects/%d/advisors/checks/%s', $projectId, $checkId), AdvisorCheck::class); + } + + /** + * List Advisor Insights + * @link https://developer.crowdin.com/api/v2/#operation/api.projects.advisors.insights.getMany API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.advisors.insights.getMany API Documentation Enterprise + * + * @param int $projectId + * @param array $params + * boolean $params[isDismissed]
+ * string $params[status]
+ * string $params[outcome]
+ * integer $params[limit]
+ * integer $params[offset] + * @return ModelCollection + */ + public function listInsights(int $projectId, array $params = []): ModelCollection + { + return $this->_list(sprintf('projects/%d/advisors/insights', $projectId), AdvisorInsight::class, $params); + } + + /** + * Edit Advisor Insight + * @link https://developer.crowdin.com/api/v2/#operation/api.projects.advisors.insights.patch API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.advisors.insights.patch API Documentation Enterprise + * + * @param int $projectId + * @param AdvisorInsight $insight Only isDismissed can be changed + * @return AdvisorInsight|null + */ + public function updateInsight(int $projectId, AdvisorInsight $insight): ?AdvisorInsight + { + return $this->_update(sprintf('projects/%d/advisors/insights/%d', $projectId, $insight->getId()), $insight); + } + + /** + * Create or Update Application Advisor Insight + * @link https://developer.crowdin.com/api/v2/#operation/api.projects.applications.modules.advisors.insights.put API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.applications.modules.advisors.insights.put API Documentation Enterprise + * + * @param int $projectId + * @param string $applicationIdentifier + * @param string $moduleKey Key of the application's advisor-inspector module + * @param array $data + * string $data[outcome] required. Enum: "flagged" "clear" "not_applicable"
+ * string $data[checkedAt]
+ * array $data[metrics]
+ * array $data[recommendations]
+ * array $data[payload] + * @return mixed + */ + public function putApplicationInsight( + int $projectId, + string $applicationIdentifier, + string $moduleKey, + array $data + ) { + return $this->client->apiRequest( + 'put', + sprintf( + 'projects/%d/applications/%s/modules/%s/advisors/insights', + $projectId, + $applicationIdentifier, + $moduleKey + ), + null, + [ + 'body' => json_encode($data), + 'headers' => $this->getHeaders(), + ] + ); + } +} diff --git a/src/CrowdinApiClient/Api/DictionaryApi.php b/src/CrowdinApiClient/Api/DictionaryApi.php new file mode 100644 index 00000000..49366d14 --- /dev/null +++ b/src/CrowdinApiClient/Api/DictionaryApi.php @@ -0,0 +1,50 @@ +_list(sprintf('projects/%d/dictionaries', $projectId), Dictionary::class, $params); + } + + /** + * Edit Dictionary + * @link https://developer.crowdin.com/api/v2/#operation/api.projects.dictionaries.patch API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.dictionaries.patch API Documentation Enterprise + * + * @param int $projectId + * @param string $languageId + * @param array $data JSON Patch array. op: add or remove, path: /words/{index}, value: word (for add) + * @return Dictionary|null + */ + public function update(int $projectId, string $languageId, array $data): ?Dictionary + { + return $this->_patch( + sprintf('projects/%d/dictionaries/%s', $projectId, $languageId), + Dictionary::class, + $data + ); + } +} diff --git a/src/CrowdinApiClient/Api/SystemPlaceholderApi.php b/src/CrowdinApiClient/Api/SystemPlaceholderApi.php new file mode 100644 index 00000000..6d901779 --- /dev/null +++ b/src/CrowdinApiClient/Api/SystemPlaceholderApi.php @@ -0,0 +1,59 @@ + + * integer $params[offset] + * @return ModelCollection + */ + public function list(int $projectId, array $params = []): ModelCollection + { + return $this->_list( + sprintf('projects/%d/system-placeholders', $projectId), + SystemPlaceholder::class, + $params + ); + } + + /** + * Project System Placeholder Batch Operations + * @link https://developer.crowdin.com/api/v2/#operation/api.projects.system-placeholders.batchPatch API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.system-placeholders.batchPatch API Documentation Enterprise + * + * @param int $projectId + * @param array $data JSON Patch array. op: replace, path: /{placeholderId}/isEnabled, value: bool + * @return ModelCollection + */ + public function batchOperations(int $projectId, array $data): ModelCollection + { + return $this->client->apiRequest( + 'patch', + sprintf('projects/%d/system-placeholders', $projectId), + new ResponseModelListDecorator(SystemPlaceholder::class), + [ + 'body' => json_encode($data), + 'headers' => $this->getHeaders(), + ] + ); + } +} diff --git a/src/CrowdinApiClient/Crowdin.php b/src/CrowdinApiClient/Crowdin.php index d1938fce..71662cdd 100644 --- a/src/CrowdinApiClient/Crowdin.php +++ b/src/CrowdinApiClient/Crowdin.php @@ -54,6 +54,9 @@ * @property \CrowdinApiClient\Api\StyleGuideApi $styleGuide * @property \CrowdinApiClient\Api\ApplicationApi $application * @property \CrowdinApiClient\Api\AiGatewayApi|\CrowdinApiClient\Api\Enterprise\AiGatewayApi $aiGateway + * @property \CrowdinApiClient\Api\DictionaryApi $dictionary + * @property \CrowdinApiClient\Api\SystemPlaceholderApi $systemPlaceholder + * @property \CrowdinApiClient\Api\AdvisorApi $advisor */ class Crowdin { @@ -130,6 +133,9 @@ class Crowdin 'styleGuide', 'application', 'aiGateway', + 'dictionary', + 'systemPlaceholder', + 'advisor', ]; protected $servicesEnterprise = [ @@ -176,6 +182,9 @@ class Crowdin 'styleGuide', 'application', 'aiGateway', + 'dictionary', + 'systemPlaceholder', + 'advisor', ]; public function __construct(array $config) diff --git a/src/CrowdinApiClient/Model/AdvisorCheck.php b/src/CrowdinApiClient/Model/AdvisorCheck.php new file mode 100644 index 00000000..7c40a644 --- /dev/null +++ b/src/CrowdinApiClient/Model/AdvisorCheck.php @@ -0,0 +1,89 @@ +identifier = (string)$this->getDataProperty('identifier'); + $this->status = (string)$this->getDataProperty('status'); + $this->progress = (int)$this->getDataProperty('progress'); + $this->attributes = (array)$this->getDataProperty('attributes'); + $this->createdAt = (string)$this->getDataProperty('createdAt'); + $this->updatedAt = (string)$this->getDataProperty('updatedAt'); + $this->startedAt = $this->nullableString('startedAt'); + $this->finishedAt = $this->nullableString('finishedAt'); + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getStatus(): string + { + return $this->status; + } + + public function getProgress(): int + { + return $this->progress; + } + + /** + * @return array category (when scoped to a category), inspectors + */ + public function getAttributes(): array + { + return $this->attributes; + } + + public function getCreatedAt(): string + { + return $this->createdAt; + } + + public function getUpdatedAt(): string + { + return $this->updatedAt; + } + + public function getStartedAt(): ?string + { + return $this->startedAt; + } + + public function getFinishedAt(): ?string + { + return $this->finishedAt; + } +} diff --git a/src/CrowdinApiClient/Model/AdvisorInsight.php b/src/CrowdinApiClient/Model/AdvisorInsight.php new file mode 100644 index 00000000..fd9d12be --- /dev/null +++ b/src/CrowdinApiClient/Model/AdvisorInsight.php @@ -0,0 +1,160 @@ +id = (int)$this->getDataProperty('id'); + $this->inspectorKey = (string)$this->getDataProperty('inspectorKey'); + $this->category = $this->nullableString('category'); + $this->isDismissed = (bool)$this->getDataProperty('isDismissed'); + $this->status = (string)$this->getDataProperty('status'); + $this->outcome = $this->nullableString('outcome'); + $this->severity = $this->nullableString('severity'); + $this->refreshPolicy = $this->nullableString('refreshPolicy'); + $this->metrics = (array)$this->getDataProperty('metrics'); + $this->recommendations = (array)$this->getDataProperty('recommendations'); + $this->checkedAt = $this->nullableString('checkedAt'); + $this->lastAiRun = $this->nullableArray('lastAiRun'); + $this->payload = $this->nullableArray('payload'); + } + + public function getId(): int + { + return $this->id; + } + + public function getInspectorKey(): string + { + return $this->inspectorKey; + } + + public function getCategory(): ?string + { + return $this->category; + } + + public function isDismissed(): bool + { + return $this->isDismissed; + } + + /** + * Dismiss or reactivate the insight; send with AdvisorApi::updateInsight() + */ + public function setIsDismissed(bool $isDismissed): void + { + $this->isDismissed = $isDismissed; + } + + /** + * @return string pending, checking, outdated or done + */ + public function getStatus(): string + { + return $this->status; + } + + /** + * @return string|null flagged, clear or not_applicable + */ + public function getOutcome(): ?string + { + return $this->outcome; + } + + /** + * @return string|null high, medium or low + */ + public function getSeverity(): ?string + { + return $this->severity; + } + + /** + * @return string|null hourly or daily + */ + public function getRefreshPolicy(): ?string + { + return $this->refreshPolicy; + } + + /** + * @return array list of key, value, unit, threshold, tone, source, checkedAt + */ + public function getMetrics(): array + { + return $this->metrics; + } + + /** + * @return array list of id, primary, params + */ + public function getRecommendations(): array + { + return $this->recommendations; + } + + public function getCheckedAt(): ?string + { + return $this->checkedAt; + } + + /** + * @return array|null mode, promptId, at + */ + public function getLastAiRun(): ?array + { + return $this->lastAiRun; + } + + public function getPayload(): ?array + { + return $this->payload; + } +} diff --git a/src/CrowdinApiClient/Model/Dictionary.php b/src/CrowdinApiClient/Model/Dictionary.php new file mode 100644 index 00000000..eadfd337 --- /dev/null +++ b/src/CrowdinApiClient/Model/Dictionary.php @@ -0,0 +1,35 @@ +languageId = (string)$this->getDataProperty('languageId'); + $this->words = (array)$this->getDataProperty('words'); + } + + public function getLanguageId(): string + { + return $this->languageId; + } + + /** + * @return string[] + */ + public function getWords(): array + { + return $this->words; + } +} diff --git a/src/CrowdinApiClient/Model/SystemPlaceholder.php b/src/CrowdinApiClient/Model/SystemPlaceholder.php new file mode 100644 index 00000000..933fe60e --- /dev/null +++ b/src/CrowdinApiClient/Model/SystemPlaceholder.php @@ -0,0 +1,65 @@ +id = (string)$this->getDataProperty('id'); + $this->isEnabled = (bool)$this->getDataProperty('isEnabled'); + $this->label = (string)$this->getDataProperty('label'); + $this->examples = (array)$this->getDataProperty('examples'); + $this->description = (string)$this->getDataProperty('description'); + } + + /** + * @return string e.g. bracesDouble, printfSpecifier + */ + public function getId(): string + { + return $this->id; + } + + public function isEnabled(): bool + { + return $this->isEnabled; + } + + public function getLabel(): string + { + return $this->label; + } + + /** + * @return string[] + */ + public function getExamples(): array + { + return $this->examples; + } + + public function getDescription(): string + { + return $this->description; + } +} diff --git a/tests/CrowdinApiClient/Api/AdvisorApiTest.php b/tests/CrowdinApiClient/Api/AdvisorApiTest.php new file mode 100644 index 00000000..09f22599 --- /dev/null +++ b/tests/CrowdinApiClient/Api/AdvisorApiTest.php @@ -0,0 +1,157 @@ + [ + 'identifier' => '50fb3506-4127-4ba8-8296-f97dc7e3e0c3', + 'status' => 'created', + 'progress' => 0, + 'attributes' => [ + 'category' => 'context', + 'inspectors' => [], + ], + 'createdAt' => '2026-09-23T11:26:54+00:00', + 'updatedAt' => '2026-09-23T11:26:54+00:00', + 'startedAt' => null, + 'finishedAt' => null, + ], + ]); + } + + private function insightData(): array + { + return [ + 'id' => 7, + 'inspectorKey' => 'missing-context', + 'category' => 'context', + 'isDismissed' => false, + 'status' => 'done', + 'outcome' => 'flagged', + 'severity' => 'high', + 'refreshPolicy' => 'daily', + 'metrics' => [ + [ + 'key' => 'coverage', + 'value' => 42, + 'unit' => 'percent', + 'threshold' => 80, + 'tone' => 'danger', + 'source' => 'deterministic', + 'checkedAt' => null, + ], + ], + 'recommendations' => [], + 'checkedAt' => '2026-09-23T11:26:54+00:00', + 'lastAiRun' => null, + 'payload' => null, + ]; + } + + public function testCreateCheck(): void + { + $data = ['category' => 'context']; + + $this->mockRequest([ + 'path' => '/projects/2/advisors/checks', + 'method' => 'post', + 'body' => json_encode($data), + 'response' => $this->checkResponse(), + ]); + + $check = $this->crowdin->advisor->createCheck(2, $data); + + $this->assertInstanceOf(AdvisorCheck::class, $check); + $this->assertEquals('50fb3506-4127-4ba8-8296-f97dc7e3e0c3', $check->getIdentifier()); + } + + public function testCreateCheckForAllInspectors(): void + { + $this->mockRequest([ + 'path' => '/projects/2/advisors/checks', + 'method' => 'post', + 'body' => '[]', + 'response' => $this->checkResponse(), + ]); + + $this->assertInstanceOf(AdvisorCheck::class, $this->crowdin->advisor->createCheck(2)); + } + + public function testGetCheck(): void + { + $this->mockRequestGet( + '/projects/2/advisors/checks/50fb3506-4127-4ba8-8296-f97dc7e3e0c3', + $this->checkResponse() + ); + + $check = $this->crowdin->advisor->getCheck(2, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + + $this->assertInstanceOf(AdvisorCheck::class, $check); + $this->assertEquals('created', $check->getStatus()); + $this->assertEquals('context', $check->getAttributes()['category']); + } + + public function testListInsights(): void + { + $this->mockRequestGet( + '/projects/2/advisors/insights?isDismissed=0&outcome=flagged', + json_encode([ + 'data' => [['data' => $this->insightData()]], + 'pagination' => ['offset' => 0, 'limit' => 25, 'total' => 1], + ]) + ); + + $insights = $this->crowdin->advisor->listInsights(2, ['isDismissed' => false, 'outcome' => 'flagged']); + + $this->assertInstanceOf(ModelCollection::class, $insights); + $this->assertCount(1, $insights); + $this->assertInstanceOf(AdvisorInsight::class, $insights[0]); + $this->assertEquals('missing-context', $insights[0]->getInspectorKey()); + } + + public function testUpdateInsight(): void + { + $insight = new AdvisorInsight($this->insightData()); + $insight->setIsDismissed(true); + + $this->mockRequest([ + 'path' => '/projects/2/advisors/insights/7', + 'method' => 'patch', + 'body' => json_encode([['op' => 'replace', 'path' => '/isDismissed', 'value' => true]]), + 'response' => json_encode(['data' => array_merge($this->insightData(), ['isDismissed' => true])]), + ]); + + $updated = $this->crowdin->advisor->updateInsight(2, $insight); + + $this->assertInstanceOf(AdvisorInsight::class, $updated); + $this->assertTrue($updated->isDismissed()); + } + + public function testPutApplicationInsight(): void + { + $data = [ + 'outcome' => 'flagged', + 'checkedAt' => '2026-09-23T11:26:54+00:00', + 'metrics' => [['key' => 'coverage', 'value' => 42, 'unit' => 'percent']], + ]; + + $this->mockRequest([ + 'path' => '/projects/2/applications/my-app/modules/my-inspector/advisors/insights', + 'method' => 'put', + 'body' => json_encode($data), + 'response' => '', + ]); + + $this->assertNull($this->crowdin->advisor->putApplicationInsight(2, 'my-app', 'my-inspector', $data)); + } +} diff --git a/tests/CrowdinApiClient/Api/DictionaryApiTest.php b/tests/CrowdinApiClient/Api/DictionaryApiTest.php new file mode 100644 index 00000000..2a0c19ed --- /dev/null +++ b/tests/CrowdinApiClient/Api/DictionaryApiTest.php @@ -0,0 +1,64 @@ +mockRequestGet( + '/projects/2/dictionaries?languageIds=uk%2Cde', + json_encode([ + 'data' => [ + [ + 'data' => [ + 'languageId' => 'uk', + 'words' => ['Crowdin', 'localization'], + ], + ], + ], + 'pagination' => [ + 'offset' => 0, + 'limit' => 25, + ], + ]) + ); + + $dictionaries = $this->crowdin->dictionary->list(2, ['languageIds' => 'uk,de']); + + $this->assertInstanceOf(ModelCollection::class, $dictionaries); + $this->assertCount(1, $dictionaries); + $this->assertInstanceOf(Dictionary::class, $dictionaries[0]); + $this->assertEquals('uk', $dictionaries[0]->getLanguageId()); + } + + public function testUpdate(): void + { + $data = [ + ['op' => 'add', 'path' => '/words/0', 'value' => 'Crowdin'], + ['op' => 'remove', 'path' => '/words/1'], + ]; + + $this->mockRequest([ + 'path' => '/projects/2/dictionaries/uk', + 'method' => 'patch', + 'body' => json_encode($data), + 'response' => json_encode([ + 'data' => [ + 'languageId' => 'uk', + 'words' => ['Crowdin'], + ], + ]), + ]); + + $dictionary = $this->crowdin->dictionary->update(2, 'uk', $data); + + $this->assertInstanceOf(Dictionary::class, $dictionary); + $this->assertEquals(['Crowdin'], $dictionary->getWords()); + } +} diff --git a/tests/CrowdinApiClient/Api/SystemPlaceholderApiTest.php b/tests/CrowdinApiClient/Api/SystemPlaceholderApiTest.php new file mode 100644 index 00000000..a1c35323 --- /dev/null +++ b/tests/CrowdinApiClient/Api/SystemPlaceholderApiTest.php @@ -0,0 +1,74 @@ +mockRequestGet( + '/projects/2/system-placeholders?limit=10', + json_encode([ + 'data' => [ + [ + 'data' => [ + 'id' => 'bracesDouble', + 'isEnabled' => true, + 'label' => '{{…}}', + 'examples' => ['{{name}}'], + 'description' => 'Double curly braces', + ], + ], + ], + 'pagination' => [ + 'offset' => 0, + 'limit' => 10, + ], + ]) + ); + + $placeholders = $this->crowdin->systemPlaceholder->list(2, ['limit' => 10]); + + $this->assertInstanceOf(ModelCollection::class, $placeholders); + $this->assertCount(1, $placeholders); + $this->assertInstanceOf(SystemPlaceholder::class, $placeholders[0]); + $this->assertEquals('bracesDouble', $placeholders[0]->getId()); + } + + public function testBatchOperations(): void + { + $data = [ + ['op' => 'replace', 'path' => '/bracesDouble/isEnabled', 'value' => false], + ]; + + $this->mockRequest([ + 'path' => '/projects/2/system-placeholders', + 'method' => 'patch', + 'body' => json_encode($data), + 'response' => json_encode([ + 'data' => [ + [ + 'data' => [ + 'id' => 'bracesDouble', + 'isEnabled' => false, + 'label' => '{{…}}', + 'examples' => ['{{name}}'], + 'description' => 'Double curly braces', + ], + ], + ], + ]), + ]); + + $placeholders = $this->crowdin->systemPlaceholder->batchOperations(2, $data); + + $this->assertInstanceOf(ModelCollection::class, $placeholders); + $this->assertCount(1, $placeholders); + $this->assertFalse($placeholders[0]->isEnabled()); + } +} diff --git a/tests/CrowdinApiClient/Model/AdvisorCheckTest.php b/tests/CrowdinApiClient/Model/AdvisorCheckTest.php new file mode 100644 index 00000000..14b410c3 --- /dev/null +++ b/tests/CrowdinApiClient/Model/AdvisorCheckTest.php @@ -0,0 +1,42 @@ + '50fb3506-4127-4ba8-8296-f97dc7e3e0c3', + 'status' => 'finished', + 'progress' => 100, + 'attributes' => ['inspectors' => [['key' => 'missing-context']]], + 'createdAt' => '2026-09-23T11:26:54+00:00', + 'updatedAt' => '2026-09-23T11:27:54+00:00', + 'startedAt' => '2026-09-23T11:26:55+00:00', + 'finishedAt' => '2026-09-23T11:27:54+00:00', + ]); + + $this->assertSame('50fb3506-4127-4ba8-8296-f97dc7e3e0c3', $check->getIdentifier()); + $this->assertSame('finished', $check->getStatus()); + $this->assertSame(100, $check->getProgress()); + $this->assertSame([['key' => 'missing-context']], $check->getAttributes()['inspectors']); + $this->assertSame('2026-09-23T11:26:54+00:00', $check->getCreatedAt()); + $this->assertSame('2026-09-23T11:27:54+00:00', $check->getUpdatedAt()); + $this->assertSame('2026-09-23T11:26:55+00:00', $check->getStartedAt()); + $this->assertSame('2026-09-23T11:27:54+00:00', $check->getFinishedAt()); + } + + public function testLoadDataWithoutOptionalFields(): void + { + $check = new AdvisorCheck(['identifier' => 'id', 'status' => 'created']); + + $this->assertNull($check->getStartedAt()); + $this->assertNull($check->getFinishedAt()); + } +} diff --git a/tests/CrowdinApiClient/Model/AdvisorInsightTest.php b/tests/CrowdinApiClient/Model/AdvisorInsightTest.php new file mode 100644 index 00000000..97ff03af --- /dev/null +++ b/tests/CrowdinApiClient/Model/AdvisorInsightTest.php @@ -0,0 +1,67 @@ + 7, + 'inspectorKey' => 'missing-context', + 'category' => 'context', + 'isDismissed' => false, + 'status' => 'done', + 'outcome' => 'flagged', + 'severity' => 'high', + 'refreshPolicy' => 'daily', + 'metrics' => [['key' => 'coverage', 'value' => 42, 'unit' => 'percent']], + 'recommendations' => [['id' => 'add-context', 'primary' => true, 'params' => []]], + 'checkedAt' => '2026-09-23T11:26:54+00:00', + 'lastAiRun' => ['mode' => 'auto', 'promptId' => 3, 'at' => '2026-09-23T11:00:00+00:00'], + 'payload' => ['stringIds' => [1, 2]], + ]; + $insight = new AdvisorInsight($data); + + $this->assertSame(7, $insight->getId()); + $this->assertSame('missing-context', $insight->getInspectorKey()); + $this->assertSame('context', $insight->getCategory()); + $this->assertFalse($insight->isDismissed()); + $this->assertSame('done', $insight->getStatus()); + $this->assertSame('flagged', $insight->getOutcome()); + $this->assertSame('high', $insight->getSeverity()); + $this->assertSame('daily', $insight->getRefreshPolicy()); + $this->assertSame($data['metrics'], $insight->getMetrics()); + $this->assertSame($data['recommendations'], $insight->getRecommendations()); + $this->assertSame('2026-09-23T11:26:54+00:00', $insight->getCheckedAt()); + $this->assertSame($data['lastAiRun'], $insight->getLastAiRun()); + $this->assertSame($data['payload'], $insight->getPayload()); + } + + public function testLoadDataWithoutOptionalFields(): void + { + $insight = new AdvisorInsight(['id' => 7, 'inspectorKey' => 'missing-context', 'status' => 'pending']); + + $this->assertNull($insight->getCategory()); + $this->assertNull($insight->getOutcome()); + $this->assertNull($insight->getSeverity()); + $this->assertNull($insight->getRefreshPolicy()); + $this->assertNull($insight->getCheckedAt()); + $this->assertNull($insight->getLastAiRun()); + $this->assertNull($insight->getPayload()); + $this->assertSame([], $insight->getMetrics()); + } + + public function testSetIsDismissed(): void + { + $insight = new AdvisorInsight(['id' => 7, 'isDismissed' => false]); + $insight->setIsDismissed(true); + + $this->assertTrue($insight->isDismissed()); + } +} diff --git a/tests/CrowdinApiClient/Model/DictionaryTest.php b/tests/CrowdinApiClient/Model/DictionaryTest.php new file mode 100644 index 00000000..5a107032 --- /dev/null +++ b/tests/CrowdinApiClient/Model/DictionaryTest.php @@ -0,0 +1,24 @@ + 'uk', 'words' => ['Crowdin', 'localization']]); + + $this->assertSame('uk', $dictionary->getLanguageId()); + $this->assertSame(['Crowdin', 'localization'], $dictionary->getWords()); + } + + public function testLoadDataWithoutWords(): void + { + $this->assertSame([], (new Dictionary(['languageId' => 'uk']))->getWords()); + } +} diff --git a/tests/CrowdinApiClient/Model/SystemPlaceholderTest.php b/tests/CrowdinApiClient/Model/SystemPlaceholderTest.php new file mode 100644 index 00000000..2ec6d3c5 --- /dev/null +++ b/tests/CrowdinApiClient/Model/SystemPlaceholderTest.php @@ -0,0 +1,28 @@ + 'printfSpecifier', + 'isEnabled' => true, + 'label' => '%s', + 'examples' => ['%s', '%1$d'], + 'description' => 'printf-style format specifiers', + ]); + + $this->assertSame('printfSpecifier', $placeholder->getId()); + $this->assertTrue($placeholder->isEnabled()); + $this->assertSame('%s', $placeholder->getLabel()); + $this->assertSame(['%s', '%1$d'], $placeholder->getExamples()); + $this->assertSame('printf-style format specifiers', $placeholder->getDescription()); + } +} From 1a7b92237fc619d5bfef46bb27b265b4531110ac Mon Sep 17 00:00:00 2001 From: Bohdan Bobko <172895427+bhdnb@users.noreply.github.com> Date: Wed, 30 Sep 2026 18:39:29 +0300 Subject: [PATCH 7/9] feat: add Enterprise placeholders, organization, clients, spellcheckers and QA checks --- .../Api/Enterprise/ClientApi.php | 31 +++ .../Api/Enterprise/CustomPlaceholderApi.php | 163 ++++++++++++++++ .../Api/Enterprise/CustomSpellcheckerApi.php | 43 ++++ .../Api/Enterprise/ExternalQaCheckApi.php | 44 +++++ .../Api/Enterprise/OrganizationApi.php | 37 ++++ .../Api/Enterprise/TaskApi.php | 44 +++++ .../Api/Enterprise/TeamApi.php | 39 ++++ .../Api/Enterprise/UserApi.php | 59 ++++++ src/CrowdinApiClient/Crowdin.php | 12 +- .../Model/Enterprise/Client.php | 64 ++++++ .../Model/Enterprise/CustomPlaceholder.php | 67 +++++++ .../Model/Enterprise/CustomSpellchecker.php | 64 ++++++ .../Model/Enterprise/ExternalQaCheck.php | 70 +++++++ .../Model/Enterprise/Organization.php | 134 +++++++++++++ .../Enterprise/OrganizationAuthSettings.php | 46 +++++ .../Model/Enterprise/ProjectContribution.php | 74 +++++++ .../Model/Enterprise/ProjectPermission.php | 62 ++++++ .../Model/Enterprise/ProjectPlaceholder.php | 102 ++++++++++ .../Api/Enterprise/ClientApiTest.php | 39 ++++ .../Enterprise/CustomPlaceholderApiTest.php | 183 ++++++++++++++++++ .../Enterprise/CustomSpellcheckerApiTest.php | 54 ++++++ .../Api/Enterprise/ExternalQaCheckApiTest.php | 50 +++++ .../Api/Enterprise/OrganizationApiTest.php | 60 ++++++ .../Api/Enterprise/TaskApiTest.php | 28 +++ .../Api/Enterprise/TeamApiTest.php | 55 ++++++ .../Api/Enterprise/UserApiTest.php | 86 ++++++++ .../Model/Enterprise/ClientTest.php | 28 +++ .../Enterprise/CustomPlaceholderTest.php | 43 ++++ .../Enterprise/CustomSpellcheckerTest.php | 39 ++++ .../Model/Enterprise/ExternalQaCheckTest.php | 38 ++++ .../OrganizationAuthSettingsTest.php | 24 +++ .../Model/Enterprise/OrganizationTest.php | 72 +++++++ .../Enterprise/ProjectContributionTest.php | 32 +++ .../Enterprise/ProjectPermissionTest.php | 35 ++++ .../Enterprise/ProjectPlaceholderTest.php | 56 ++++++ 35 files changed, 2076 insertions(+), 1 deletion(-) create mode 100644 src/CrowdinApiClient/Api/Enterprise/ClientApi.php create mode 100644 src/CrowdinApiClient/Api/Enterprise/CustomPlaceholderApi.php create mode 100644 src/CrowdinApiClient/Api/Enterprise/CustomSpellcheckerApi.php create mode 100644 src/CrowdinApiClient/Api/Enterprise/ExternalQaCheckApi.php create mode 100644 src/CrowdinApiClient/Api/Enterprise/OrganizationApi.php create mode 100644 src/CrowdinApiClient/Api/Enterprise/TaskApi.php create mode 100644 src/CrowdinApiClient/Model/Enterprise/Client.php create mode 100644 src/CrowdinApiClient/Model/Enterprise/CustomPlaceholder.php create mode 100644 src/CrowdinApiClient/Model/Enterprise/CustomSpellchecker.php create mode 100644 src/CrowdinApiClient/Model/Enterprise/ExternalQaCheck.php create mode 100644 src/CrowdinApiClient/Model/Enterprise/Organization.php create mode 100644 src/CrowdinApiClient/Model/Enterprise/OrganizationAuthSettings.php create mode 100644 src/CrowdinApiClient/Model/Enterprise/ProjectContribution.php create mode 100644 src/CrowdinApiClient/Model/Enterprise/ProjectPermission.php create mode 100644 src/CrowdinApiClient/Model/Enterprise/ProjectPlaceholder.php create mode 100644 tests/CrowdinApiClient/Api/Enterprise/ClientApiTest.php create mode 100644 tests/CrowdinApiClient/Api/Enterprise/CustomPlaceholderApiTest.php create mode 100644 tests/CrowdinApiClient/Api/Enterprise/CustomSpellcheckerApiTest.php create mode 100644 tests/CrowdinApiClient/Api/Enterprise/ExternalQaCheckApiTest.php create mode 100644 tests/CrowdinApiClient/Api/Enterprise/OrganizationApiTest.php create mode 100644 tests/CrowdinApiClient/Model/Enterprise/ClientTest.php create mode 100644 tests/CrowdinApiClient/Model/Enterprise/CustomPlaceholderTest.php create mode 100644 tests/CrowdinApiClient/Model/Enterprise/CustomSpellcheckerTest.php create mode 100644 tests/CrowdinApiClient/Model/Enterprise/ExternalQaCheckTest.php create mode 100644 tests/CrowdinApiClient/Model/Enterprise/OrganizationAuthSettingsTest.php create mode 100644 tests/CrowdinApiClient/Model/Enterprise/OrganizationTest.php create mode 100644 tests/CrowdinApiClient/Model/Enterprise/ProjectContributionTest.php create mode 100644 tests/CrowdinApiClient/Model/Enterprise/ProjectPermissionTest.php create mode 100644 tests/CrowdinApiClient/Model/Enterprise/ProjectPlaceholderTest.php diff --git a/src/CrowdinApiClient/Api/Enterprise/ClientApi.php b/src/CrowdinApiClient/Api/Enterprise/ClientApi.php new file mode 100644 index 00000000..c185e4f8 --- /dev/null +++ b/src/CrowdinApiClient/Api/Enterprise/ClientApi.php @@ -0,0 +1,31 @@ + + * integer $params[offset] + * @return ModelCollection + */ + public function list(array $params = []): ModelCollection + { + return $this->_list('clients', Client::class, $params); + } +} diff --git a/src/CrowdinApiClient/Api/Enterprise/CustomPlaceholderApi.php b/src/CrowdinApiClient/Api/Enterprise/CustomPlaceholderApi.php new file mode 100644 index 00000000..e0a86dcc --- /dev/null +++ b/src/CrowdinApiClient/Api/Enterprise/CustomPlaceholderApi.php @@ -0,0 +1,163 @@ + + * integer $params[offset] + * @return ModelCollection + */ + public function list(array $params = []): ModelCollection + { + return $this->_list('custom-placeholders', CustomPlaceholder::class, $params); + } + + /** + * Add Custom Placeholder + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.custom-placeholders.post API Documentation + * + * @param array $data + * string $data[definition] required
+ * string $data[description]
+ * string $data[argumentDelimiter] Default: " + * @return CustomPlaceholder|null + */ + public function create(array $data): ?CustomPlaceholder + { + return $this->_create('custom-placeholders', CustomPlaceholder::class, $data); + } + + /** + * Get Custom Placeholder + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.custom-placeholders.get API Documentation + * + * @param int $customPlaceholderId + * @return CustomPlaceholder|null + */ + public function get(int $customPlaceholderId): ?CustomPlaceholder + { + return $this->_get('custom-placeholders/' . $customPlaceholderId, CustomPlaceholder::class); + } + + /** + * Edit Custom Placeholder + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.custom-placeholders.patch API Documentation + * + * @param CustomPlaceholder $customPlaceholder + * @return CustomPlaceholder|null + */ + public function update(CustomPlaceholder $customPlaceholder): ?CustomPlaceholder + { + return $this->_update('custom-placeholders/' . $customPlaceholder->getId(), $customPlaceholder); + } + + /** + * Delete Custom Placeholder + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.custom-placeholders.delete API Documentation + * + * @param int $customPlaceholderId + * @return mixed + */ + public function delete(int $customPlaceholderId) + { + return $this->_delete('custom-placeholders/' . $customPlaceholderId); + } + + /** + * List Project Placeholders + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.placeholders.getMany API Documentation + * + * @param int $projectId + * @param array $params + * integer $params[limit]
+ * integer $params[offset] + * @return ModelCollection + */ + public function listProjectPlaceholders(int $projectId, array $params = []): ModelCollection + { + return $this->_list(sprintf('projects/%d/placeholders', $projectId), ProjectPlaceholder::class, $params); + } + + /** + * Add Project Placeholder + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.placeholders.post API Documentation + * + * @param int $projectId + * @param array $data + * integer $data[customPlaceholderId] required
+ * string $data[type] Enum: "high" "low"
+ * integer $data[index]
+ * boolean $data[isBlocking]
+ * string[] $data[formats] Empty means every file format + * @return ProjectPlaceholder|null + */ + public function addProjectPlaceholder(int $projectId, array $data): ?ProjectPlaceholder + { + return $this->_create(sprintf('projects/%d/placeholders', $projectId), ProjectPlaceholder::class, $data); + } + + /** + * Get Project Placeholder + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.placeholders.get API Documentation + * + * @param int $projectId + * @param int $projectPlaceholderId + * @return ProjectPlaceholder|null + */ + public function getProjectPlaceholder(int $projectId, int $projectPlaceholderId): ?ProjectPlaceholder + { + return $this->_get( + sprintf('projects/%d/placeholders/%d', $projectId, $projectPlaceholderId), + ProjectPlaceholder::class + ); + } + + /** + * Edit Project Placeholder + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.placeholders.patch API Documentation + * + * @param int $projectId + * @param ProjectPlaceholder $projectPlaceholder + * @return ProjectPlaceholder|null + */ + public function updateProjectPlaceholder( + int $projectId, + ProjectPlaceholder $projectPlaceholder + ): ?ProjectPlaceholder { + return $this->_update( + sprintf('projects/%d/placeholders/%d', $projectId, $projectPlaceholder->getId()), + $projectPlaceholder + ); + } + + /** + * Delete Project Placeholder + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.placeholders.delete API Documentation + * + * @param int $projectId + * @param int $projectPlaceholderId + * @return mixed + */ + public function deleteProjectPlaceholder(int $projectId, int $projectPlaceholderId) + { + return $this->_delete(sprintf('projects/%d/placeholders/%d', $projectId, $projectPlaceholderId)); + } +} diff --git a/src/CrowdinApiClient/Api/Enterprise/CustomSpellcheckerApi.php b/src/CrowdinApiClient/Api/Enterprise/CustomSpellcheckerApi.php new file mode 100644 index 00000000..c47ed817 --- /dev/null +++ b/src/CrowdinApiClient/Api/Enterprise/CustomSpellcheckerApi.php @@ -0,0 +1,43 @@ + + * integer $params[offset] + * @return ModelCollection + */ + public function list(array $params = []): ModelCollection + { + return $this->_list('custom-spellcheckers', CustomSpellchecker::class, $params); + } + + /** + * Get Custom Spellchecker + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.custom-spellcheckers.get API Documentation + * + * @param int $id + * @return CustomSpellchecker|null + */ + public function get(int $id): ?CustomSpellchecker + { + return $this->_get('custom-spellcheckers/' . $id, CustomSpellchecker::class); + } +} diff --git a/src/CrowdinApiClient/Api/Enterprise/ExternalQaCheckApi.php b/src/CrowdinApiClient/Api/Enterprise/ExternalQaCheckApi.php new file mode 100644 index 00000000..0c8406a9 --- /dev/null +++ b/src/CrowdinApiClient/Api/Enterprise/ExternalQaCheckApi.php @@ -0,0 +1,44 @@ + + * integer $params[limit]
+ * integer $params[offset] + * @return ModelCollection + */ + public function list(array $params = []): ModelCollection + { + return $this->_list('external-qa-checks', ExternalQaCheck::class, $params); + } + + /** + * Get External QA Check + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.external-qa-checks.get API Documentation + * + * @param int $id + * @return ExternalQaCheck|null + */ + public function get(int $id): ?ExternalQaCheck + { + return $this->_get('external-qa-checks/' . $id, ExternalQaCheck::class); + } +} diff --git a/src/CrowdinApiClient/Api/Enterprise/OrganizationApi.php b/src/CrowdinApiClient/Api/Enterprise/OrganizationApi.php new file mode 100644 index 00000000..5320264f --- /dev/null +++ b/src/CrowdinApiClient/Api/Enterprise/OrganizationApi.php @@ -0,0 +1,37 @@ +_get('organization', Organization::class); + } + + /** + * Get Organization Authentication Settings + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.organization.auth-settings.get API Documentation + * + * @return OrganizationAuthSettings|null + */ + public function getAuthSettings(): ?OrganizationAuthSettings + { + return $this->_get('organization/auth-settings', OrganizationAuthSettings::class); + } +} diff --git a/src/CrowdinApiClient/Api/Enterprise/TaskApi.php b/src/CrowdinApiClient/Api/Enterprise/TaskApi.php new file mode 100644 index 00000000..5cd89712 --- /dev/null +++ b/src/CrowdinApiClient/Api/Enterprise/TaskApi.php @@ -0,0 +1,44 @@ + + * integer $params[limit] [1 .. 500] Default: 25
+ * integer $params[offset] >= 0 Default: 0
+ * string $params[status] Enum: "todo" "in_progress" "done" "closed"
+ * string $params[type]
+ * string $params[projectIds]
+ * string $params[groupIds]
+ * string $params[assigneeIds]
+ * string $params[creatorIds]
+ * string $params[targetLanguageIds]
+ * string $params[sourceLanguageIds]
+ * string $params[createdAtFrom]
+ * string $params[createdAtTo]
+ * string $params[deadlineFrom]
+ * string $params[deadlineTo] + * @return ModelCollection + */ + public function listOrganizationTasks(array $params = []): ModelCollection + { + return $this->_list('tasks', Task::class, $params); + } +} diff --git a/src/CrowdinApiClient/Api/Enterprise/TeamApi.php b/src/CrowdinApiClient/Api/Enterprise/TeamApi.php index b045816a..210b8a48 100644 --- a/src/CrowdinApiClient/Api/Enterprise/TeamApi.php +++ b/src/CrowdinApiClient/Api/Enterprise/TeamApi.php @@ -3,7 +3,9 @@ namespace CrowdinApiClient\Api\Enterprise; use CrowdinApiClient\Api\AbstractApi; +use CrowdinApiClient\Http\ResponseDecorator\ResponseModelListDecorator; use CrowdinApiClient\Model\Enterprise\AddedProjectTeamInfo; +use CrowdinApiClient\Model\Enterprise\ProjectPermission; use CrowdinApiClient\Model\Enterprise\Team; use CrowdinApiClient\ModelCollection; @@ -93,4 +95,41 @@ public function update(Team $team): Team { return $this->_update('teams/' . $team->getId(), $team); } + + /** + * List Team Projects Permissions + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.teams.projects.permissions.getMany API Documentation + * + * @param int $teamId + * @param array $params + * integer $params[limit]
+ * integer $params[offset] + * @return ModelCollection + */ + public function listProjectPermissions(int $teamId, array $params = []): ModelCollection + { + return $this->_list(sprintf('teams/%d/projects/permissions', $teamId), ProjectPermission::class, $params); + } + + /** + * Permissions Batch Operations + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.teams.projects.permissions.patch API Documentation + * + * @param int $teamId + * @param array $data JSON Patch array. op: add, replace or remove, path: /{projectId}/roles
+ * value: list of role name and permissions + * @return ModelCollection + */ + public function updateProjectPermissions(int $teamId, array $data): ModelCollection + { + return $this->client->apiRequest( + 'patch', + sprintf('teams/%d/projects/permissions', $teamId), + new ResponseModelListDecorator(ProjectPermission::class), + [ + 'body' => json_encode($data), + 'headers' => $this->getHeaders(), + ] + ); + } } diff --git a/src/CrowdinApiClient/Api/Enterprise/UserApi.php b/src/CrowdinApiClient/Api/Enterprise/UserApi.php index cc49e2cf..d05c83d2 100644 --- a/src/CrowdinApiClient/Api/Enterprise/UserApi.php +++ b/src/CrowdinApiClient/Api/Enterprise/UserApi.php @@ -3,6 +3,9 @@ namespace CrowdinApiClient\Api\Enterprise; use CrowdinApiClient\Api\AbstractApi; +use CrowdinApiClient\Http\ResponseDecorator\ResponseModelListDecorator; +use CrowdinApiClient\Model\Enterprise\ProjectContribution; +use CrowdinApiClient\Model\Enterprise\ProjectPermission; use CrowdinApiClient\Model\Enterprise\ProjectTeamMemberAddedStatistics; use CrowdinApiClient\Model\Enterprise\ProjectTeamMemberResource; use CrowdinApiClient\Model\Enterprise\User; @@ -158,4 +161,60 @@ public function update(User $user): ?User { return $this->_update('users/' . $user->getId(), $user); } + + /** + * List User Projects Permissions + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.users.projects.permissions.getMany API Documentation + * + * @param int $userId + * @param array $params + * integer $params[limit]
+ * integer $params[offset] + * @return ModelCollection + */ + public function listProjectPermissions(int $userId, array $params = []): ModelCollection + { + return $this->_list(sprintf('users/%d/projects/permissions', $userId), ProjectPermission::class, $params); + } + + /** + * Permissions Batch Operations + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.users.projects.permissions.patch API Documentation + * + * @param int $userId + * @param array $data JSON Patch array. op: add, replace or remove, path: /{projectId}/roles
+ * value: list of role name and permissions + * @return ModelCollection + */ + public function updateProjectPermissions(int $userId, array $data): ModelCollection + { + return $this->client->apiRequest( + 'patch', + sprintf('users/%d/projects/permissions', $userId), + new ResponseModelListDecorator(ProjectPermission::class), + [ + 'body' => json_encode($data), + 'headers' => $this->getHeaders(), + ] + ); + } + + /** + * List User Projects Contributions + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.users.projects.contributions.getMany API Documentation + * + * @param int $userId + * @param array $params + * integer $params[limit]
+ * integer $params[offset] + * @return ModelCollection + */ + public function listProjectContributions(int $userId, array $params = []): ModelCollection + { + return $this->_list( + sprintf('users/%d/projects/contributions', $userId), + ProjectContribution::class, + $params + ); + } } diff --git a/src/CrowdinApiClient/Crowdin.php b/src/CrowdinApiClient/Crowdin.php index 71662cdd..652f5df0 100644 --- a/src/CrowdinApiClient/Crowdin.php +++ b/src/CrowdinApiClient/Crowdin.php @@ -18,7 +18,7 @@ * @property \CrowdinApiClient\Api\Enterprise\GroupApi $group * @property \CrowdinApiClient\Api\ProjectApi $project * @property \CrowdinApiClient\Api\BranchApi $branch - * @property \CrowdinApiClient\Api\TaskApi $task + * @property \CrowdinApiClient\Api\TaskApi|\CrowdinApiClient\Api\Enterprise\TaskApi $task * @property \CrowdinApiClient\Api\TaskCommentApi $taskComment * @property \CrowdinApiClient\Api\IssueApi $issue * @property \CrowdinApiClient\Api\ScreenshotApi $screenshot @@ -57,6 +57,11 @@ * @property \CrowdinApiClient\Api\DictionaryApi $dictionary * @property \CrowdinApiClient\Api\SystemPlaceholderApi $systemPlaceholder * @property \CrowdinApiClient\Api\AdvisorApi $advisor + * @property \CrowdinApiClient\Api\Enterprise\CustomPlaceholderApi $customPlaceholder + * @property \CrowdinApiClient\Api\Enterprise\OrganizationApi $organization + * @property \CrowdinApiClient\Api\Enterprise\ClientApi $client + * @property \CrowdinApiClient\Api\Enterprise\CustomSpellcheckerApi $customSpellchecker + * @property \CrowdinApiClient\Api\Enterprise\ExternalQaCheckApi $externalQaCheck */ class Crowdin { @@ -185,6 +190,11 @@ class Crowdin 'dictionary', 'systemPlaceholder', 'advisor', + 'customPlaceholder', + 'organization', + 'client', + 'customSpellchecker', + 'externalQaCheck', ]; public function __construct(array $config) diff --git a/src/CrowdinApiClient/Model/Enterprise/Client.php b/src/CrowdinApiClient/Model/Enterprise/Client.php new file mode 100644 index 00000000..31a55f34 --- /dev/null +++ b/src/CrowdinApiClient/Model/Enterprise/Client.php @@ -0,0 +1,64 @@ +id = (int)$this->getDataProperty('id'); + $this->name = (string)$this->getDataProperty('name'); + $this->description = (string)$this->getDataProperty('description'); + $this->status = (string)$this->getDataProperty('status'); + $this->webUrl = (string)$this->getDataProperty('webUrl'); + } + + public function getId(): int + { + return $this->id; + } + + public function getName(): string + { + return $this->name; + } + + public function getDescription(): string + { + return $this->description; + } + + /** + * @return string pending, confirmed or rejected + */ + public function getStatus(): string + { + return $this->status; + } + + public function getWebUrl(): string + { + return $this->webUrl; + } +} diff --git a/src/CrowdinApiClient/Model/Enterprise/CustomPlaceholder.php b/src/CrowdinApiClient/Model/Enterprise/CustomPlaceholder.php new file mode 100644 index 00000000..1bd4998c --- /dev/null +++ b/src/CrowdinApiClient/Model/Enterprise/CustomPlaceholder.php @@ -0,0 +1,67 @@ +id = (int)$this->getDataProperty('id'); + $this->description = $this->nullableString('description'); + $this->definition = (string)$this->getDataProperty('definition'); + $this->argumentDelimiter = (string)$this->getDataProperty('argumentDelimiter'); + } + + public function getId(): int + { + return $this->id; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(string $description): void + { + $this->description = $description; + } + + public function getDefinition(): string + { + return $this->definition; + } + + public function setDefinition(string $definition): void + { + $this->definition = $definition; + } + + public function getArgumentDelimiter(): string + { + return $this->argumentDelimiter; + } + + public function setArgumentDelimiter(string $argumentDelimiter): void + { + $this->argumentDelimiter = $argumentDelimiter; + } +} diff --git a/src/CrowdinApiClient/Model/Enterprise/CustomSpellchecker.php b/src/CrowdinApiClient/Model/Enterprise/CustomSpellchecker.php new file mode 100644 index 00000000..48d5a529 --- /dev/null +++ b/src/CrowdinApiClient/Model/Enterprise/CustomSpellchecker.php @@ -0,0 +1,64 @@ +id = (int)$this->getDataProperty('id'); + $this->name = (string)$this->getDataProperty('name'); + $this->config = (array)$this->getDataProperty('config'); + $this->createdAt = (string)$this->getDataProperty('createdAt'); + $this->updatedAt = $this->nullableString('updatedAt'); + } + + public function getId(): int + { + return $this->id; + } + + public function getName(): string + { + return $this->name; + } + + /** + * @return array identifier, key, realTimeCheckEnabled, enabledLanguageIds + */ + public function getConfig(): array + { + return $this->config; + } + + public function getCreatedAt(): string + { + return $this->createdAt; + } + + public function getUpdatedAt(): ?string + { + return $this->updatedAt; + } +} diff --git a/src/CrowdinApiClient/Model/Enterprise/ExternalQaCheck.php b/src/CrowdinApiClient/Model/Enterprise/ExternalQaCheck.php new file mode 100644 index 00000000..dc821941 --- /dev/null +++ b/src/CrowdinApiClient/Model/Enterprise/ExternalQaCheck.php @@ -0,0 +1,70 @@ +id = (int)$this->getDataProperty('id'); + $this->name = (string)$this->getDataProperty('name'); + $this->description = $this->nullableString('description'); + $this->config = (array)$this->getDataProperty('config'); + $this->createdAt = (string)$this->getDataProperty('createdAt'); + $this->updatedAt = $this->nullableString('updatedAt'); + } + + public function getId(): int + { + return $this->id; + } + + public function getName(): string + { + return $this->name; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function getConfig(): array + { + return $this->config; + } + + public function getCreatedAt(): string + { + return $this->createdAt; + } + + public function getUpdatedAt(): ?string + { + return $this->updatedAt; + } +} diff --git a/src/CrowdinApiClient/Model/Enterprise/Organization.php b/src/CrowdinApiClient/Model/Enterprise/Organization.php new file mode 100644 index 00000000..601b0a02 --- /dev/null +++ b/src/CrowdinApiClient/Model/Enterprise/Organization.php @@ -0,0 +1,134 @@ +id = (int)$this->getDataProperty('id'); + $this->domain = (string)$this->getDataProperty('domain'); + $this->name = (string)$this->getDataProperty('name'); + $this->logo = $this->nullableString('logo'); + $this->defaultLogo = (string)$this->getDataProperty('defaultLogo'); + $this->description = $this->nullableString('description'); + $this->internalDescription = $this->nullableString('internalDescription'); + $this->cname = $this->nullableString('cname'); + // The spec types isVendor as a string; FILTER_VALIDATE_BOOLEAN keeps "false" false, unlike a (bool) cast + $this->isVendor = filter_var($this->getDataProperty('isVendor'), FILTER_VALIDATE_BOOLEAN); + $this->defaultPublicProjectsView = (string)$this->getDataProperty('defaultPublicProjectsView'); + $this->plan = (array)$this->getDataProperty('plan'); + $this->defaults = (array)$this->getDataProperty('defaults'); + } + + public function getId(): int + { + return $this->id; + } + + public function getDomain(): string + { + return $this->domain; + } + + public function getName(): string + { + return $this->name; + } + + public function getLogo(): ?string + { + return $this->logo; + } + + public function getDefaultLogo(): string + { + return $this->defaultLogo; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function getInternalDescription(): ?string + { + return $this->internalDescription; + } + + public function getCname(): ?string + { + return $this->cname; + } + + public function isVendor(): bool + { + return $this->isVendor; + } + + /** + * @return string grid or list + */ + public function getDefaultPublicProjectsView(): string + { + return $this->defaultPublicProjectsView; + } + + /** + * @return array name, wordsLimit, managersLimit + */ + public function getPlan(): array + { + return $this->plan; + } + + /** + * @return array list of name, isLocked, defaultValue + */ + public function getDefaults(): array + { + return $this->defaults; + } +} diff --git a/src/CrowdinApiClient/Model/Enterprise/OrganizationAuthSettings.php b/src/CrowdinApiClient/Model/Enterprise/OrganizationAuthSettings.php new file mode 100644 index 00000000..bd92afc8 --- /dev/null +++ b/src/CrowdinApiClient/Model/Enterprise/OrganizationAuthSettings.php @@ -0,0 +1,46 @@ +allowSignUp = (bool)$this->getDataProperty('allowSignUp'); + $this->twoFactorAuthentication = (bool)$this->getDataProperty('twoFactorAuthentication'); + $this->authMethods = (array)$this->getDataProperty('authMethods'); + } + + public function getAllowSignUp(): bool + { + return $this->allowSignUp; + } + + public function getTwoFactorAuthentication(): bool + { + return $this->twoFactorAuthentication; + } + + /** + * @return array list of name, isEnabled, isDefault + */ + public function getAuthMethods(): array + { + return $this->authMethods; + } +} diff --git a/src/CrowdinApiClient/Model/Enterprise/ProjectContribution.php b/src/CrowdinApiClient/Model/Enterprise/ProjectContribution.php new file mode 100644 index 00000000..805e3d6a --- /dev/null +++ b/src/CrowdinApiClient/Model/Enterprise/ProjectContribution.php @@ -0,0 +1,74 @@ +id = (int)$this->getDataProperty('id'); + $this->translated = (array)$this->getDataProperty('translated'); + $this->approved = (array)$this->getDataProperty('approved'); + $this->voted = (array)$this->getDataProperty('voted'); + $this->commented = (array)$this->getDataProperty('commented'); + $this->project = new Project((array)$this->getDataProperty('project')); + } + + public function getId(): int + { + return $this->id; + } + + public function getTranslated(): array + { + return $this->translated; + } + + public function getApproved(): array + { + return $this->approved; + } + + public function getVoted(): array + { + return $this->voted; + } + + public function getCommented(): array + { + return $this->commented; + } + + public function getProject(): Project + { + return $this->project; + } +} diff --git a/src/CrowdinApiClient/Model/Enterprise/ProjectPermission.php b/src/CrowdinApiClient/Model/Enterprise/ProjectPermission.php new file mode 100644 index 00000000..d72bb164 --- /dev/null +++ b/src/CrowdinApiClient/Model/Enterprise/ProjectPermission.php @@ -0,0 +1,62 @@ +id = (int)$this->getDataProperty('id'); + $this->roles = (array)$this->getDataProperty('roles'); + $this->project = new Project((array)$this->getDataProperty('project')); + $this->teams = (array)$this->getDataProperty('teams'); + } + + public function getId(): int + { + return $this->id; + } + + /** + * @return array list of name, permissions + */ + public function getRoles(): array + { + return $this->roles; + } + + public function getProject(): Project + { + return $this->project; + } + + /** + * @return array teams that grant the user access to the project; empty for team permissions + */ + public function getTeams(): array + { + return $this->teams; + } +} diff --git a/src/CrowdinApiClient/Model/Enterprise/ProjectPlaceholder.php b/src/CrowdinApiClient/Model/Enterprise/ProjectPlaceholder.php new file mode 100644 index 00000000..6745594a --- /dev/null +++ b/src/CrowdinApiClient/Model/Enterprise/ProjectPlaceholder.php @@ -0,0 +1,102 @@ +id = $this->nullableInt('id'); + $this->customPlaceholderId = $this->nullableInt('customPlaceholderId'); + $this->type = $this->nullableString('type'); + $this->index = $this->nullableInt('index'); + $this->isBlocking = $this->nullableBool('isBlocking'); + $this->formats = (array)$this->getDataProperty('formats'); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getCustomPlaceholderId(): ?int + { + return $this->customPlaceholderId; + } + + /** + * @return string|null high or low + */ + public function getType(): ?string + { + return $this->type; + } + + /** + * @param string $type high or low + */ + public function setType(string $type): void + { + $this->type = $type; + } + + public function getIndex(): ?int + { + return $this->index; + } + + public function setIndex(int $index): void + { + $this->index = $index; + } + + public function getIsBlocking(): ?bool + { + return $this->isBlocking; + } + + public function setIsBlocking(bool $isBlocking): void + { + $this->isBlocking = $isBlocking; + } + + /** + * @return string[] empty means every file format + */ + public function getFormats(): array + { + return $this->formats; + } + + /** + * @param string[] $formats empty means every file format + */ + public function setFormats(array $formats): void + { + $this->formats = $formats; + } +} diff --git a/tests/CrowdinApiClient/Api/Enterprise/ClientApiTest.php b/tests/CrowdinApiClient/Api/Enterprise/ClientApiTest.php new file mode 100644 index 00000000..c39cd54b --- /dev/null +++ b/tests/CrowdinApiClient/Api/Enterprise/ClientApiTest.php @@ -0,0 +1,39 @@ +mockRequestGet( + '/clients?limit=10', + json_encode([ + 'data' => [ + [ + 'data' => [ + 'id' => 1, + 'name' => 'Umbrella', + 'description' => 'Client organization', + 'status' => 'confirmed', + 'webUrl' => 'https://umbrella.crowdin.com', + ], + ], + ], + 'pagination' => ['offset' => 0, 'limit' => 10], + ]) + ); + + $clients = $this->crowdin->client->list(['limit' => 10]); + + $this->assertInstanceOf(ModelCollection::class, $clients); + $this->assertCount(1, $clients); + $this->assertInstanceOf(Client::class, $clients[0]); + $this->assertEquals('confirmed', $clients[0]->getStatus()); + } +} diff --git a/tests/CrowdinApiClient/Api/Enterprise/CustomPlaceholderApiTest.php b/tests/CrowdinApiClient/Api/Enterprise/CustomPlaceholderApiTest.php new file mode 100644 index 00000000..52760ff1 --- /dev/null +++ b/tests/CrowdinApiClient/Api/Enterprise/CustomPlaceholderApiTest.php @@ -0,0 +1,183 @@ + 3, + 'description' => 'URL', + 'definition' => 'start, then "http", maybe "s", then "://", anything but " ", end', + 'argumentDelimiter' => '"', + ]; + } + + private function projectPlaceholderData(): array + { + return [ + 'id' => 5, + 'customPlaceholderId' => 3, + 'type' => 'high', + 'index' => 1, + 'isBlocking' => false, + 'formats' => ['docbook'], + ]; + } + + public function testList(): void + { + $this->mockRequestGet( + '/custom-placeholders?limit=10', + json_encode([ + 'data' => [['data' => $this->customPlaceholderData()]], + 'pagination' => ['offset' => 0, 'limit' => 10], + ]) + ); + + $placeholders = $this->crowdin->customPlaceholder->list(['limit' => 10]); + + $this->assertInstanceOf(ModelCollection::class, $placeholders); + $this->assertCount(1, $placeholders); + $this->assertInstanceOf(CustomPlaceholder::class, $placeholders[0]); + $this->assertEquals(3, $placeholders[0]->getId()); + } + + public function testCreate(): void + { + $data = ['definition' => 'start, then "http", end', 'description' => 'URL']; + + $this->mockRequest([ + 'path' => '/custom-placeholders', + 'method' => 'post', + 'body' => json_encode($data), + 'response' => json_encode(['data' => $this->customPlaceholderData()]), + ]); + + $placeholder = $this->crowdin->customPlaceholder->create($data); + + $this->assertInstanceOf(CustomPlaceholder::class, $placeholder); + $this->assertEquals('URL', $placeholder->getDescription()); + } + + public function testGet(): void + { + $this->mockRequestGet('/custom-placeholders/3', json_encode(['data' => $this->customPlaceholderData()])); + + $placeholder = $this->crowdin->customPlaceholder->get(3); + + $this->assertInstanceOf(CustomPlaceholder::class, $placeholder); + $this->assertEquals('"', $placeholder->getArgumentDelimiter()); + } + + public function testUpdate(): void + { + $placeholder = new CustomPlaceholder($this->customPlaceholderData()); + $placeholder->setDescription('Web link'); + + $this->mockRequest([ + 'path' => '/custom-placeholders/3', + 'method' => 'patch', + 'body' => json_encode([['op' => 'replace', 'path' => '/description', 'value' => 'Web link']]), + 'response' => json_encode([ + 'data' => array_merge($this->customPlaceholderData(), ['description' => 'Web link']), + ]), + ]); + + $updated = $this->crowdin->customPlaceholder->update($placeholder); + + $this->assertInstanceOf(CustomPlaceholder::class, $updated); + $this->assertEquals('Web link', $updated->getDescription()); + } + + public function testDelete(): void + { + $this->mockRequestDelete('/custom-placeholders/3'); + + $this->crowdin->customPlaceholder->delete(3); + } + + public function testListProjectPlaceholders(): void + { + $this->mockRequestGet( + '/projects/2/placeholders', + json_encode([ + 'data' => [['data' => $this->projectPlaceholderData()]], + 'pagination' => ['offset' => 0, 'limit' => 25], + ]) + ); + + $placeholders = $this->crowdin->customPlaceholder->listProjectPlaceholders(2); + + $this->assertInstanceOf(ModelCollection::class, $placeholders); + $this->assertCount(1, $placeholders); + $this->assertInstanceOf(ProjectPlaceholder::class, $placeholders[0]); + $this->assertEquals(3, $placeholders[0]->getCustomPlaceholderId()); + } + + public function testAddProjectPlaceholder(): void + { + $data = ['customPlaceholderId' => 3, 'type' => 'high', 'formats' => ['docbook']]; + + $this->mockRequest([ + 'path' => '/projects/2/placeholders', + 'method' => 'post', + 'body' => json_encode($data), + 'response' => json_encode(['data' => $this->projectPlaceholderData()]), + ]); + + $placeholder = $this->crowdin->customPlaceholder->addProjectPlaceholder(2, $data); + + $this->assertInstanceOf(ProjectPlaceholder::class, $placeholder); + $this->assertEquals(5, $placeholder->getId()); + } + + public function testGetProjectPlaceholder(): void + { + $this->mockRequestGet('/projects/2/placeholders/5', json_encode(['data' => $this->projectPlaceholderData()])); + + $placeholder = $this->crowdin->customPlaceholder->getProjectPlaceholder(2, 5); + + $this->assertInstanceOf(ProjectPlaceholder::class, $placeholder); + $this->assertEquals('high', $placeholder->getType()); + } + + public function testUpdateProjectPlaceholder(): void + { + $placeholder = new ProjectPlaceholder($this->projectPlaceholderData()); + $placeholder->setIsBlocking(true); + $placeholder->setFormats([]); + + $this->mockRequest([ + 'path' => '/projects/2/placeholders/5', + 'method' => 'patch', + 'body' => json_encode([ + ['op' => 'replace', 'path' => '/isBlocking', 'value' => true], + ['op' => 'replace', 'path' => '/formats', 'value' => []], + ]), + 'response' => json_encode([ + 'data' => array_merge($this->projectPlaceholderData(), ['isBlocking' => true, 'formats' => []]), + ]), + ]); + + $updated = $this->crowdin->customPlaceholder->updateProjectPlaceholder(2, $placeholder); + + $this->assertInstanceOf(ProjectPlaceholder::class, $updated); + $this->assertTrue($updated->getIsBlocking()); + $this->assertEquals([], $updated->getFormats()); + } + + public function testDeleteProjectPlaceholder(): void + { + $this->mockRequestDelete('/projects/2/placeholders/5'); + + $this->crowdin->customPlaceholder->deleteProjectPlaceholder(2, 5); + } +} diff --git a/tests/CrowdinApiClient/Api/Enterprise/CustomSpellcheckerApiTest.php b/tests/CrowdinApiClient/Api/Enterprise/CustomSpellcheckerApiTest.php new file mode 100644 index 00000000..5f43faa8 --- /dev/null +++ b/tests/CrowdinApiClient/Api/Enterprise/CustomSpellcheckerApiTest.php @@ -0,0 +1,54 @@ + 4, + 'name' => 'Company terms', + 'config' => [ + 'identifier' => 'company-terms', + 'key' => 'spellchecker', + 'realTimeCheckEnabled' => true, + 'enabledLanguageIds' => ['uk'], + ], + 'createdAt' => '2026-09-01T10:00:00+00:00', + 'updatedAt' => null, + ]; + } + + public function testList(): void + { + $this->mockRequestGet( + '/custom-spellcheckers', + json_encode([ + 'data' => [['data' => $this->spellcheckerData()]], + 'pagination' => ['offset' => 0, 'limit' => 25], + ]) + ); + + $spellcheckers = $this->crowdin->customSpellchecker->list(); + + $this->assertInstanceOf(ModelCollection::class, $spellcheckers); + $this->assertCount(1, $spellcheckers); + $this->assertInstanceOf(CustomSpellchecker::class, $spellcheckers[0]); + } + + public function testGet(): void + { + $this->mockRequestGet('/custom-spellcheckers/4', json_encode(['data' => $this->spellcheckerData()])); + + $spellchecker = $this->crowdin->customSpellchecker->get(4); + + $this->assertInstanceOf(CustomSpellchecker::class, $spellchecker); + $this->assertEquals('Company terms', $spellchecker->getName()); + } +} diff --git a/tests/CrowdinApiClient/Api/Enterprise/ExternalQaCheckApiTest.php b/tests/CrowdinApiClient/Api/Enterprise/ExternalQaCheckApiTest.php new file mode 100644 index 00000000..d981bff9 --- /dev/null +++ b/tests/CrowdinApiClient/Api/Enterprise/ExternalQaCheckApiTest.php @@ -0,0 +1,50 @@ + 6, + 'name' => 'Brand voice', + 'description' => null, + 'config' => ['identifier' => 'brand-voice'], + 'createdAt' => '2026-09-01T10:00:00+00:00', + 'updatedAt' => '2026-09-02T10:00:00+00:00', + ]; + } + + public function testList(): void + { + $this->mockRequestGet( + '/external-qa-checks?projectId=2', + json_encode([ + 'data' => [['data' => $this->qaCheckData()]], + 'pagination' => ['offset' => 0, 'limit' => 25], + ]) + ); + + $qaChecks = $this->crowdin->externalQaCheck->list(['projectId' => 2]); + + $this->assertInstanceOf(ModelCollection::class, $qaChecks); + $this->assertCount(1, $qaChecks); + $this->assertInstanceOf(ExternalQaCheck::class, $qaChecks[0]); + } + + public function testGet(): void + { + $this->mockRequestGet('/external-qa-checks/6', json_encode(['data' => $this->qaCheckData()])); + + $qaCheck = $this->crowdin->externalQaCheck->get(6); + + $this->assertInstanceOf(ExternalQaCheck::class, $qaCheck); + $this->assertEquals('Brand voice', $qaCheck->getName()); + } +} diff --git a/tests/CrowdinApiClient/Api/Enterprise/OrganizationApiTest.php b/tests/CrowdinApiClient/Api/Enterprise/OrganizationApiTest.php new file mode 100644 index 00000000..674933c9 --- /dev/null +++ b/tests/CrowdinApiClient/Api/Enterprise/OrganizationApiTest.php @@ -0,0 +1,60 @@ +mockRequestGet( + '/organization', + json_encode([ + 'data' => [ + 'id' => 200000999, + 'domain' => 'acme', + 'name' => 'Acme', + 'logo' => null, + 'defaultLogo' => 'https://crowdin.com/logo.png', + 'description' => null, + 'internalDescription' => null, + 'cname' => null, + 'isVendor' => false, + 'defaultPublicProjectsView' => 'grid', + 'plan' => ['name' => 'Business', 'wordsLimit' => null, 'managersLimit' => 10], + 'defaults' => [], + ], + ]) + ); + + $organization = $this->crowdin->organization->get(); + + $this->assertInstanceOf(Organization::class, $organization); + $this->assertEquals('acme', $organization->getDomain()); + $this->assertFalse($organization->isVendor()); + } + + public function testGetAuthSettings(): void + { + $this->mockRequestGet( + '/organization/auth-settings', + json_encode([ + 'data' => [ + 'allowSignUp' => false, + 'twoFactorAuthentication' => true, + 'authMethods' => [['name' => 'saml', 'isEnabled' => true, 'isDefault' => true]], + ], + ]) + ); + + $settings = $this->crowdin->organization->getAuthSettings(); + + $this->assertInstanceOf(OrganizationAuthSettings::class, $settings); + $this->assertTrue($settings->getTwoFactorAuthentication()); + $this->assertEquals('saml', $settings->getAuthMethods()[0]['name']); + } +} diff --git a/tests/CrowdinApiClient/Api/Enterprise/TaskApiTest.php b/tests/CrowdinApiClient/Api/Enterprise/TaskApiTest.php index 3737cc08..8a850a9b 100644 --- a/tests/CrowdinApiClient/Api/Enterprise/TaskApiTest.php +++ b/tests/CrowdinApiClient/Api/Enterprise/TaskApiTest.php @@ -245,4 +245,32 @@ public function testDelete() $this->mockRequestDelete('/projects/2/tasks/2'); $this->crowdin->task->delete(2, 2); } + + public function testListOrganizationTasks(): void + { + $this->mockRequestGet( + '/tasks?status=todo&projectIds=1%2C2', + json_encode([ + 'data' => [ + [ + 'data' => [ + 'id' => 2, + 'projectId' => 1, + 'type' => 0, + 'status' => 'todo', + 'title' => 'French', + ], + ], + ], + 'pagination' => ['offset' => 0, 'limit' => 25], + ]) + ); + + $tasks = $this->crowdin->task->listOrganizationTasks(['status' => 'todo', 'projectIds' => '1,2']); + + $this->assertInstanceOf(ModelCollection::class, $tasks); + $this->assertCount(1, $tasks); + $this->assertInstanceOf(Task::class, $tasks[0]); + $this->assertEquals('French', $tasks[0]->getTitle()); + } } diff --git a/tests/CrowdinApiClient/Api/Enterprise/TeamApiTest.php b/tests/CrowdinApiClient/Api/Enterprise/TeamApiTest.php index 3b4f99d3..06ac7eee 100644 --- a/tests/CrowdinApiClient/Api/Enterprise/TeamApiTest.php +++ b/tests/CrowdinApiClient/Api/Enterprise/TeamApiTest.php @@ -3,6 +3,7 @@ namespace CrowdinApiClient\Tests\Api\Enterprise; use CrowdinApiClient\Model\Enterprise\AddedProjectTeamInfo; +use CrowdinApiClient\Model\Enterprise\ProjectPermission; use CrowdinApiClient\Model\Enterprise\Team; use CrowdinApiClient\ModelCollection; @@ -153,4 +154,58 @@ public function testAddTeamToProject() $this->assertInstanceOf(AddedProjectTeamInfo::class, $addedProjectTeamInfo); $this->assertEquals(3, $addedProjectTeamInfo->getAdded()->getId()); } + + public function testListProjectPermissions(): void + { + $this->mockRequestGet( + '/teams/7/projects/permissions', + json_encode([ + 'data' => [ + [ + 'data' => [ + 'id' => 2, + 'roles' => [['name' => 'translator', 'permissions' => ['allLanguages' => true]]], + 'project' => ['id' => 2, 'name' => 'Knowledge Base', 'identifier' => 'knowledge-base'], + ], + ], + ], + 'pagination' => ['offset' => 0, 'limit' => 25], + ]) + ); + + $permissions = $this->crowdin->team->listProjectPermissions(7); + + $this->assertInstanceOf(ModelCollection::class, $permissions); + $this->assertCount(1, $permissions); + $this->assertInstanceOf(ProjectPermission::class, $permissions[0]); + $this->assertEquals('Knowledge Base', $permissions[0]->getProject()->getName()); + $this->assertEquals('translator', $permissions[0]->getRoles()[0]['name']); + } + + public function testUpdateProjectPermissions(): void + { + $data = [['op' => 'add', 'path' => '/2/roles', 'value' => [['name' => 'translator']]]]; + + $this->mockRequest([ + 'path' => '/teams/7/projects/permissions', + 'method' => 'patch', + 'body' => json_encode($data), + 'response' => json_encode([ + 'data' => [ + [ + 'data' => [ + 'id' => 2, + 'roles' => [['name' => 'translator', 'permissions' => ['allLanguages' => true]]], + 'project' => ['id' => 2, 'name' => 'Knowledge Base', 'identifier' => 'knowledge-base'], + ], + ], + ], + ]), + ]); + + $permissions = $this->crowdin->team->updateProjectPermissions(7, $data); + + $this->assertInstanceOf(ModelCollection::class, $permissions); + $this->assertInstanceOf(ProjectPermission::class, $permissions[0]); + } } diff --git a/tests/CrowdinApiClient/Api/Enterprise/UserApiTest.php b/tests/CrowdinApiClient/Api/Enterprise/UserApiTest.php index 32cb5d98..ca32f58f 100644 --- a/tests/CrowdinApiClient/Api/Enterprise/UserApiTest.php +++ b/tests/CrowdinApiClient/Api/Enterprise/UserApiTest.php @@ -2,6 +2,8 @@ namespace CrowdinApiClient\Tests\Api\Enterprise; +use CrowdinApiClient\Model\Enterprise\ProjectContribution; +use CrowdinApiClient\Model\Enterprise\ProjectPermission; use CrowdinApiClient\Model\Enterprise\ProjectTeamMemberAddedStatistics; use CrowdinApiClient\Model\Enterprise\ProjectTeamMemberResource; use CrowdinApiClient\Model\Enterprise\User; @@ -352,4 +354,88 @@ public function testGetAndUpdateUser() $this->assertEquals(1, $user->getId()); $this->assertEquals('Joe', $user->getFirstName()); } + + public function testListProjectPermissions(): void + { + $this->mockRequestGet( + '/users/7/projects/permissions', + json_encode([ + 'data' => [ + [ + 'data' => [ + 'id' => 2, + 'roles' => [['name' => 'translator', 'permissions' => ['allLanguages' => true]]], + 'project' => ['id' => 2, 'name' => 'Knowledge Base', 'identifier' => 'knowledge-base'], + ], + ], + ], + 'pagination' => ['offset' => 0, 'limit' => 25], + ]) + ); + + $permissions = $this->crowdin->user->listProjectPermissions(7); + + $this->assertInstanceOf(ModelCollection::class, $permissions); + $this->assertCount(1, $permissions); + $this->assertInstanceOf(ProjectPermission::class, $permissions[0]); + $this->assertEquals('Knowledge Base', $permissions[0]->getProject()->getName()); + $this->assertEquals('translator', $permissions[0]->getRoles()[0]['name']); + } + + public function testUpdateProjectPermissions(): void + { + $data = [['op' => 'add', 'path' => '/2/roles', 'value' => [['name' => 'translator']]]]; + + $this->mockRequest([ + 'path' => '/users/7/projects/permissions', + 'method' => 'patch', + 'body' => json_encode($data), + 'response' => json_encode([ + 'data' => [ + [ + 'data' => [ + 'id' => 2, + 'roles' => [['name' => 'translator', 'permissions' => ['allLanguages' => true]]], + 'project' => ['id' => 2, 'name' => 'Knowledge Base', 'identifier' => 'knowledge-base'], + ], + ], + ], + ]), + ]); + + $permissions = $this->crowdin->user->updateProjectPermissions(7, $data); + + $this->assertInstanceOf(ModelCollection::class, $permissions); + $this->assertInstanceOf(ProjectPermission::class, $permissions[0]); + } + + public function testListProjectContributions(): void + { + $this->mockRequestGet( + '/users/7/projects/contributions', + json_encode([ + 'data' => [ + [ + 'data' => [ + 'id' => 2, + 'translated' => ['strings' => 10, 'words' => 42], + 'approved' => ['strings' => 3, 'words' => 12], + 'voted' => ['strings' => 0, 'words' => 0], + 'commented' => ['strings' => 1, 'words' => 0], + 'project' => ['id' => 2, 'name' => 'Knowledge Base', 'identifier' => 'knowledge-base'], + ], + ], + ], + 'pagination' => ['offset' => 0, 'limit' => 25], + ]) + ); + + $contributions = $this->crowdin->user->listProjectContributions(7); + + $this->assertInstanceOf(ModelCollection::class, $contributions); + $this->assertCount(1, $contributions); + $this->assertInstanceOf(ProjectContribution::class, $contributions[0]); + $this->assertEquals(42, $contributions[0]->getTranslated()['words']); + $this->assertEquals(2, $contributions[0]->getProject()->getId()); + } } diff --git a/tests/CrowdinApiClient/Model/Enterprise/ClientTest.php b/tests/CrowdinApiClient/Model/Enterprise/ClientTest.php new file mode 100644 index 00000000..5167e7f1 --- /dev/null +++ b/tests/CrowdinApiClient/Model/Enterprise/ClientTest.php @@ -0,0 +1,28 @@ + 1, + 'name' => 'Umbrella', + 'description' => 'Client organization', + 'status' => 'pending', + 'webUrl' => 'https://umbrella.crowdin.com', + ]); + + $this->assertSame(1, $client->getId()); + $this->assertSame('Umbrella', $client->getName()); + $this->assertSame('Client organization', $client->getDescription()); + $this->assertSame('pending', $client->getStatus()); + $this->assertSame('https://umbrella.crowdin.com', $client->getWebUrl()); + } +} diff --git a/tests/CrowdinApiClient/Model/Enterprise/CustomPlaceholderTest.php b/tests/CrowdinApiClient/Model/Enterprise/CustomPlaceholderTest.php new file mode 100644 index 00000000..cfb5199d --- /dev/null +++ b/tests/CrowdinApiClient/Model/Enterprise/CustomPlaceholderTest.php @@ -0,0 +1,43 @@ + 3, + 'description' => 'URL', + 'definition' => 'start, then "http", end', + 'argumentDelimiter' => '"', + ]); + + $this->assertSame(3, $placeholder->getId()); + $this->assertSame('URL', $placeholder->getDescription()); + $this->assertSame('start, then "http", end', $placeholder->getDefinition()); + $this->assertSame('"', $placeholder->getArgumentDelimiter()); + } + + public function testLoadDataWithoutDescription(): void + { + $this->assertNull((new CustomPlaceholder(['id' => 3, 'definition' => 'start, end']))->getDescription()); + } + + public function testSetData(): void + { + $placeholder = new CustomPlaceholder(); + $placeholder->setDescription('Web link'); + $placeholder->setDefinition('start, end'); + $placeholder->setArgumentDelimiter("'"); + + $this->assertSame('Web link', $placeholder->getDescription()); + $this->assertSame('start, end', $placeholder->getDefinition()); + $this->assertSame("'", $placeholder->getArgumentDelimiter()); + } +} diff --git a/tests/CrowdinApiClient/Model/Enterprise/CustomSpellcheckerTest.php b/tests/CrowdinApiClient/Model/Enterprise/CustomSpellcheckerTest.php new file mode 100644 index 00000000..51e5c49f --- /dev/null +++ b/tests/CrowdinApiClient/Model/Enterprise/CustomSpellcheckerTest.php @@ -0,0 +1,39 @@ + 'company-terms', + 'key' => 'spellchecker', + 'realTimeCheckEnabled' => true, + 'enabledLanguageIds' => ['uk'], + ]; + $spellchecker = new CustomSpellchecker([ + 'id' => 4, + 'name' => 'Company terms', + 'config' => $config, + 'createdAt' => '2026-09-01T10:00:00+00:00', + 'updatedAt' => '2026-09-02T10:00:00+00:00', + ]); + + $this->assertSame(4, $spellchecker->getId()); + $this->assertSame('Company terms', $spellchecker->getName()); + $this->assertSame($config, $spellchecker->getConfig()); + $this->assertSame('2026-09-01T10:00:00+00:00', $spellchecker->getCreatedAt()); + $this->assertSame('2026-09-02T10:00:00+00:00', $spellchecker->getUpdatedAt()); + } + + public function testLoadDataWithoutUpdatedAt(): void + { + $this->assertNull((new CustomSpellchecker(['id' => 4]))->getUpdatedAt()); + } +} diff --git a/tests/CrowdinApiClient/Model/Enterprise/ExternalQaCheckTest.php b/tests/CrowdinApiClient/Model/Enterprise/ExternalQaCheckTest.php new file mode 100644 index 00000000..370e6991 --- /dev/null +++ b/tests/CrowdinApiClient/Model/Enterprise/ExternalQaCheckTest.php @@ -0,0 +1,38 @@ + 6, + 'name' => 'Brand voice', + 'description' => 'Checks brand terms', + 'config' => ['identifier' => 'brand-voice'], + 'createdAt' => '2026-09-01T10:00:00+00:00', + 'updatedAt' => '2026-09-02T10:00:00+00:00', + ]); + + $this->assertSame(6, $qaCheck->getId()); + $this->assertSame('Brand voice', $qaCheck->getName()); + $this->assertSame('Checks brand terms', $qaCheck->getDescription()); + $this->assertSame(['identifier' => 'brand-voice'], $qaCheck->getConfig()); + $this->assertSame('2026-09-01T10:00:00+00:00', $qaCheck->getCreatedAt()); + $this->assertSame('2026-09-02T10:00:00+00:00', $qaCheck->getUpdatedAt()); + } + + public function testLoadDataWithoutOptionalFields(): void + { + $qaCheck = new ExternalQaCheck(['id' => 6, 'name' => 'Brand voice']); + + $this->assertNull($qaCheck->getDescription()); + $this->assertNull($qaCheck->getUpdatedAt()); + } +} diff --git a/tests/CrowdinApiClient/Model/Enterprise/OrganizationAuthSettingsTest.php b/tests/CrowdinApiClient/Model/Enterprise/OrganizationAuthSettingsTest.php new file mode 100644 index 00000000..12918280 --- /dev/null +++ b/tests/CrowdinApiClient/Model/Enterprise/OrganizationAuthSettingsTest.php @@ -0,0 +1,24 @@ + true, + 'twoFactorAuthentication' => false, + 'authMethods' => [['name' => 'saml', 'isEnabled' => true, 'isDefault' => true]], + ]); + + $this->assertTrue($settings->getAllowSignUp()); + $this->assertFalse($settings->getTwoFactorAuthentication()); + $this->assertSame([['name' => 'saml', 'isEnabled' => true, 'isDefault' => true]], $settings->getAuthMethods()); + } +} diff --git a/tests/CrowdinApiClient/Model/Enterprise/OrganizationTest.php b/tests/CrowdinApiClient/Model/Enterprise/OrganizationTest.php new file mode 100644 index 00000000..f80cfb4c --- /dev/null +++ b/tests/CrowdinApiClient/Model/Enterprise/OrganizationTest.php @@ -0,0 +1,72 @@ + 200000999, + 'domain' => 'acme', + 'name' => 'Acme', + 'logo' => 'https://acme.dev/logo.png', + 'defaultLogo' => 'https://crowdin.com/logo.png', + 'description' => 'Public description', + 'internalDescription' => 'Internal notes', + 'cname' => 'l10n.acme.dev', + 'isVendor' => true, + 'defaultPublicProjectsView' => 'list', + 'plan' => ['name' => 'Business', 'wordsLimit' => 100000, 'managersLimit' => 10], + 'defaults' => [['name' => 'projectVisibility', 'isLocked' => true, 'defaultValue' => 'private']], + ]); + + $this->assertSame(200000999, $organization->getId()); + $this->assertSame('acme', $organization->getDomain()); + $this->assertSame('Acme', $organization->getName()); + $this->assertSame('https://acme.dev/logo.png', $organization->getLogo()); + $this->assertSame('https://crowdin.com/logo.png', $organization->getDefaultLogo()); + $this->assertSame('Public description', $organization->getDescription()); + $this->assertSame('Internal notes', $organization->getInternalDescription()); + $this->assertSame('l10n.acme.dev', $organization->getCname()); + $this->assertTrue($organization->isVendor()); + $this->assertSame('list', $organization->getDefaultPublicProjectsView()); + $this->assertSame('Business', $organization->getPlan()['name']); + $this->assertSame('projectVisibility', $organization->getDefaults()[0]['name']); + } + + public function testLoadDataWithoutOptionalFields(): void + { + $organization = new Organization(['id' => 1, 'domain' => 'acme', 'name' => 'Acme']); + + $this->assertNull($organization->getLogo()); + $this->assertNull($organization->getDescription()); + $this->assertNull($organization->getInternalDescription()); + $this->assertNull($organization->getCname()); + $this->assertFalse($organization->isVendor()); + } + + /** + * @dataProvider isVendorProvider + * @param mixed $value + */ + public function testIsVendorAcceptsStringValues($value, bool $expected): void + { + $this->assertSame($expected, (new Organization(['isVendor' => $value]))->isVendor()); + } + + public function isVendorProvider(): array + { + return [ + 'string false' => ['false', false], + 'string true' => ['true', true], + 'bool false' => [false, false], + 'bool true' => [true, true], + ]; + } +} diff --git a/tests/CrowdinApiClient/Model/Enterprise/ProjectContributionTest.php b/tests/CrowdinApiClient/Model/Enterprise/ProjectContributionTest.php new file mode 100644 index 00000000..278daa92 --- /dev/null +++ b/tests/CrowdinApiClient/Model/Enterprise/ProjectContributionTest.php @@ -0,0 +1,32 @@ + 2, + 'translated' => ['strings' => 10, 'words' => 42], + 'approved' => ['strings' => 3, 'words' => 12], + 'voted' => ['strings' => 1, 'words' => 4], + 'commented' => ['strings' => 2, 'words' => 0], + 'project' => ['id' => 2, 'name' => 'Knowledge Base'], + ]); + + $this->assertSame(2, $contribution->getId()); + $this->assertSame(['strings' => 10, 'words' => 42], $contribution->getTranslated()); + $this->assertSame(['strings' => 3, 'words' => 12], $contribution->getApproved()); + $this->assertSame(['strings' => 1, 'words' => 4], $contribution->getVoted()); + $this->assertSame(['strings' => 2, 'words' => 0], $contribution->getCommented()); + $this->assertInstanceOf(Project::class, $contribution->getProject()); + $this->assertSame(2, $contribution->getProject()->getId()); + } +} diff --git a/tests/CrowdinApiClient/Model/Enterprise/ProjectPermissionTest.php b/tests/CrowdinApiClient/Model/Enterprise/ProjectPermissionTest.php new file mode 100644 index 00000000..3efe6f7a --- /dev/null +++ b/tests/CrowdinApiClient/Model/Enterprise/ProjectPermissionTest.php @@ -0,0 +1,35 @@ + 2, + 'roles' => [['name' => 'translator', 'permissions' => ['allLanguages' => true]]], + 'project' => ['id' => 2, 'name' => 'Knowledge Base'], + 'teams' => [['id' => 1, 'name' => 'Translators']], + ]); + + $this->assertSame(2, $permission->getId()); + $this->assertSame('translator', $permission->getRoles()[0]['name']); + $this->assertInstanceOf(Project::class, $permission->getProject()); + $this->assertSame('Knowledge Base', $permission->getProject()->getName()); + $this->assertSame([['id' => 1, 'name' => 'Translators']], $permission->getTeams()); + } + + public function testLoadDataWithoutTeams(): void + { + $permission = new ProjectPermission(['id' => 2, 'roles' => [], 'project' => ['id' => 2]]); + + $this->assertSame([], $permission->getTeams()); + } +} diff --git a/tests/CrowdinApiClient/Model/Enterprise/ProjectPlaceholderTest.php b/tests/CrowdinApiClient/Model/Enterprise/ProjectPlaceholderTest.php new file mode 100644 index 00000000..52a5e08e --- /dev/null +++ b/tests/CrowdinApiClient/Model/Enterprise/ProjectPlaceholderTest.php @@ -0,0 +1,56 @@ + 5, + 'customPlaceholderId' => 3, + 'type' => 'high', + 'index' => 1, + 'isBlocking' => true, + 'formats' => ['docbook', 'adoc'], + ]); + + $this->assertSame(5, $placeholder->getId()); + $this->assertSame(3, $placeholder->getCustomPlaceholderId()); + $this->assertSame('high', $placeholder->getType()); + $this->assertSame(1, $placeholder->getIndex()); + $this->assertTrue($placeholder->getIsBlocking()); + $this->assertSame(['docbook', 'adoc'], $placeholder->getFormats()); + } + + public function testLoadDataWithoutOptionalFields(): void + { + $placeholder = new ProjectPlaceholder([]); + + $this->assertNull($placeholder->getId()); + $this->assertNull($placeholder->getCustomPlaceholderId()); + $this->assertNull($placeholder->getType()); + $this->assertNull($placeholder->getIndex()); + $this->assertNull($placeholder->getIsBlocking()); + $this->assertSame([], $placeholder->getFormats()); + } + + public function testSetData(): void + { + $placeholder = new ProjectPlaceholder(); + $placeholder->setType('low'); + $placeholder->setIndex(2); + $placeholder->setIsBlocking(false); + $placeholder->setFormats(['json']); + + $this->assertSame('low', $placeholder->getType()); + $this->assertSame(2, $placeholder->getIndex()); + $this->assertFalse($placeholder->getIsBlocking()); + $this->assertSame(['json'], $placeholder->getFormats()); + } +} From 669758df4379cadd3ce7c60b88e9bb1380c7bbb3 Mon Sep 17 00:00:00 2001 From: Bohdan Bobko <172895427+bhdnb@users.noreply.github.com> Date: Wed, 30 Sep 2026 19:35:12 +0300 Subject: [PATCH 8/9] feat: add new response fields to existing models --- src/CrowdinApiClient/Model/AiReport.php | 9 + .../Model/AiRequestLogExport.php | 14 - src/CrowdinApiClient/Model/AiSettings.php | 88 ++++ src/CrowdinApiClient/Model/Bundle.php | 54 +++ src/CrowdinApiClient/Model/Directory.php | 11 + .../Model/DistributionRelease.php | 22 + .../Model/Enterprise/Vendor.php | 11 + .../Model/Enterprise/WorkflowTemplate.php | 47 +- src/CrowdinApiClient/Model/Glossary.php | 16 + .../Model/GlossaryConcept.php | 11 + src/CrowdinApiClient/Model/Label.php | 34 +- src/CrowdinApiClient/Model/Language.php | 11 + .../Model/LanguageTranslation.php | 77 ++++ .../Model/MachineTranslationEngine.php | 70 +++ src/CrowdinApiClient/Model/Progress.php | 11 + src/CrowdinApiClient/Model/ProgressFile.php | 16 +- .../Model/ProgressLanguage.php | 21 +- src/CrowdinApiClient/Model/Project.php | 410 ++++++++++++++++++ src/CrowdinApiClient/Model/ReportArchive.php | 22 + src/CrowdinApiClient/Model/SourceString.php | 11 + src/CrowdinApiClient/Model/StringComment.php | 66 +++ .../Model/StringTranslation.php | 78 ++++ .../Model/StringTranslationApproval.php | 11 + src/CrowdinApiClient/Model/StyleGuide.php | 14 + src/CrowdinApiClient/Model/Task.php | 33 ++ src/CrowdinApiClient/Model/Term.php | 16 + .../Model/TranslationMemory.php | 54 +++ .../Model/TranslationProjectBuild.php | 44 ++ tests/CrowdinApiClient/Model/AiReportTest.php | 27 ++ .../CrowdinApiClient/Model/AiSettingsTest.php | 61 +++ tests/CrowdinApiClient/Model/BundleTest.php | 35 ++ .../CrowdinApiClient/Model/DirectoryTest.php | 16 + .../Model/DistributionReleaseTest.php | 19 + .../Model/Enterprise/VendorTest.php | 16 + .../Model/Enterprise/WorkflowTemplateTest.php | 19 + .../Model/GlossaryConceptTest.php | 16 + tests/CrowdinApiClient/Model/GlossaryTest.php | 24 + tests/CrowdinApiClient/Model/LabelTest.php | 19 + tests/CrowdinApiClient/Model/LanguageTest.php | 16 + .../Model/LanguageTranslationTest.php | 45 ++ .../Model/MachineTranslationEngineTest.php | 40 ++ .../Model/ProgressFileTest.php | 17 + .../Model/ProgressLanguageTest.php | 19 + tests/CrowdinApiClient/Model/ProgressTest.php | 17 + tests/CrowdinApiClient/Model/ProjectTest.php | 148 +++++++ .../Model/ReportArchiveTest.php | 30 ++ .../Model/SourceStringTest.php | 16 + .../Model/StringCommentTest.php | 31 ++ .../Model/StringTranslationApprovalTest.php | 16 + .../Model/StringTranslationTest.php | 41 ++ .../CrowdinApiClient/Model/StyleGuideTest.php | 24 + tests/CrowdinApiClient/Model/TaskTest.php | 22 + tests/CrowdinApiClient/Model/TermTest.php | 24 + .../Model/TranslationMemoryTest.php | 35 ++ .../Model/TranslationProjectBuildTest.php | 25 ++ 55 files changed, 2046 insertions(+), 54 deletions(-) create mode 100644 tests/CrowdinApiClient/Model/AiReportTest.php create mode 100644 tests/CrowdinApiClient/Model/AiSettingsTest.php create mode 100644 tests/CrowdinApiClient/Model/LanguageTranslationTest.php create mode 100644 tests/CrowdinApiClient/Model/ReportArchiveTest.php diff --git a/src/CrowdinApiClient/Model/AiReport.php b/src/CrowdinApiClient/Model/AiReport.php index 8ba2d5a3..15a1a125 100644 --- a/src/CrowdinApiClient/Model/AiReport.php +++ b/src/CrowdinApiClient/Model/AiReport.php @@ -30,6 +30,9 @@ class AiReport extends BaseModel /** @var string|null */ protected $finishedAt; + /** @var string|null */ + protected $eta; + public function __construct(array $data = []) { parent::__construct($data); @@ -42,6 +45,7 @@ public function __construct(array $data = []) $this->updatedAt = $this->getDataProperty('updatedAt'); $this->startedAt = $this->getDataProperty('startedAt'); $this->finishedAt = $this->getDataProperty('finishedAt'); + $this->eta = $this->nullableString('eta'); } public function getIdentifier(): string @@ -83,4 +87,9 @@ public function getFinishedAt(): ?string { return $this->finishedAt; } + + public function getEta(): ?string + { + return $this->eta; + } } diff --git a/src/CrowdinApiClient/Model/AiRequestLogExport.php b/src/CrowdinApiClient/Model/AiRequestLogExport.php index eddbaf03..597e7f2a 100644 --- a/src/CrowdinApiClient/Model/AiRequestLogExport.php +++ b/src/CrowdinApiClient/Model/AiRequestLogExport.php @@ -6,18 +6,4 @@ class AiRequestLogExport extends AiReport { - /** @var string|null */ - protected $eta; - - public function __construct(array $data = []) - { - parent::__construct($data); - - $this->eta = $this->nullableString('eta'); - } - - public function getEta(): ?string - { - return $this->eta; - } } diff --git a/src/CrowdinApiClient/Model/AiSettings.php b/src/CrowdinApiClient/Model/AiSettings.php index f58c2b5d..4cc71e91 100644 --- a/src/CrowdinApiClient/Model/AiSettings.php +++ b/src/CrowdinApiClient/Model/AiSettings.php @@ -18,6 +18,27 @@ class AiSettings extends BaseModel /** @var int|null */ protected $contextReviewAiPromptId; + /** @var int|null */ + protected $alignmentActionAiPromptId; + + /** @var float|null */ + protected $dailyCostLimit; + + /** @var bool */ + protected $isLimitingActive; + + /** @var float|null */ + protected $monthlyCostLimit; + + /** @var array */ + protected $perUserOverrides; + + /** @var float|null */ + protected $userDailyCostLimit; + + /** @var float|null */ + protected $userMonthlyCostLimit; + public function __construct(array $data = []) { parent::__construct($data); @@ -26,6 +47,13 @@ public function __construct(array $data = []) $this->editorSuggestionAiPromptId = (int)$this->getDataProperty('editorSuggestionAiPromptId'); $this->qaCheckActionAiPromptId = $this->getDataProperty('qaCheckActionAiPromptId'); $this->contextReviewAiPromptId = $this->getDataProperty('contextReviewAiPromptId'); + $this->alignmentActionAiPromptId = $this->nullableInt('alignmentActionAiPromptId'); + $this->dailyCostLimit = $this->nullableFloat('dailyCostLimit'); + $this->isLimitingActive = (bool)$this->getDataProperty('isLimitingActive'); + $this->monthlyCostLimit = $this->nullableFloat('monthlyCostLimit'); + $this->perUserOverrides = (array)$this->getDataProperty('perUserOverrides'); + $this->userDailyCostLimit = $this->nullableFloat('userDailyCostLimit'); + $this->userMonthlyCostLimit = $this->nullableFloat('userMonthlyCostLimit'); } public function getPreTranslationAiPromptId(): int @@ -67,4 +95,64 @@ public function setContextReviewAiPromptId(?int $contextReviewAiPromptId): void { $this->contextReviewAiPromptId = $contextReviewAiPromptId; } + + public function getAlignmentActionAiPromptId(): ?int + { + return $this->alignmentActionAiPromptId; + } + + public function setAlignmentActionAiPromptId(int $alignmentActionAiPromptId): void + { + $this->alignmentActionAiPromptId = $alignmentActionAiPromptId; + } + + public function getDailyCostLimit(): ?float + { + return $this->dailyCostLimit; + } + + public function setDailyCostLimit(float $dailyCostLimit): void + { + $this->dailyCostLimit = $dailyCostLimit; + } + + public function isLimitingActive(): bool + { + return $this->isLimitingActive; + } + + public function getMonthlyCostLimit(): ?float + { + return $this->monthlyCostLimit; + } + + public function setMonthlyCostLimit(float $monthlyCostLimit): void + { + $this->monthlyCostLimit = $monthlyCostLimit; + } + + public function getPerUserOverrides(): array + { + return $this->perUserOverrides; + } + + public function getUserDailyCostLimit(): ?float + { + return $this->userDailyCostLimit; + } + + public function setUserDailyCostLimit(float $userDailyCostLimit): void + { + $this->userDailyCostLimit = $userDailyCostLimit; + } + + public function getUserMonthlyCostLimit(): ?float + { + return $this->userMonthlyCostLimit; + } + + public function setUserMonthlyCostLimit(float $userMonthlyCostLimit): void + { + $this->userMonthlyCostLimit = $userMonthlyCostLimit; + } } diff --git a/src/CrowdinApiClient/Model/Bundle.php b/src/CrowdinApiClient/Model/Bundle.php index 7a13edcf..3ea83ad4 100644 --- a/src/CrowdinApiClient/Model/Bundle.php +++ b/src/CrowdinApiClient/Model/Bundle.php @@ -77,6 +77,26 @@ class Bundle extends BaseModel */ protected $updatedAt; + /** + * @var string|null + */ + protected $excludeLabelMatchRule; + + /** + * @var string|null + */ + protected $labelMatchRule; + + /** + * @var array|null + */ + protected $languageIds; + + /** + * @var string + */ + protected $sourceLanguageExportPattern; + public function __construct(array $data = []) { parent::__construct($data); @@ -95,6 +115,10 @@ public function __construct(array $data = []) $this->webUrl = (string)$this->getDataProperty('webUrl'); $this->createdAt = (string)$this->getDataProperty('createdAt'); $this->updatedAt = (string)$this->getDataProperty('updatedAt'); + $this->excludeLabelMatchRule = $this->nullableString('excludeLabelMatchRule'); + $this->labelMatchRule = $this->nullableString('labelMatchRule'); + $this->languageIds = $this->nullableArray('languageIds'); + $this->sourceLanguageExportPattern = (string)$this->getDataProperty('sourceLanguageExportPattern'); } public function getId(): int @@ -240,4 +264,34 @@ public function getUpdatedAt(): string { return $this->updatedAt; } + + public function getExcludeLabelMatchRule(): ?string + { + return $this->excludeLabelMatchRule; + } + + public function setExcludeLabelMatchRule(string $excludeLabelMatchRule): void + { + $this->excludeLabelMatchRule = $excludeLabelMatchRule; + } + + public function getLabelMatchRule(): ?string + { + return $this->labelMatchRule; + } + + public function setLabelMatchRule(string $labelMatchRule): void + { + $this->labelMatchRule = $labelMatchRule; + } + + public function getLanguageIds(): ?array + { + return $this->languageIds; + } + + public function getSourceLanguageExportPattern(): string + { + return $this->sourceLanguageExportPattern; + } } diff --git a/src/CrowdinApiClient/Model/Directory.php b/src/CrowdinApiClient/Model/Directory.php index 82d9e03d..6a5b0456 100644 --- a/src/CrowdinApiClient/Model/Directory.php +++ b/src/CrowdinApiClient/Model/Directory.php @@ -57,6 +57,11 @@ class Directory extends BaseModel */ protected $updatedAt; + /** + * @var string + */ + protected $path; + public function __construct(array $data = []) { parent::__construct($data); @@ -71,6 +76,7 @@ public function __construct(array $data = []) $this->priority = (string)$this->getDataProperty('priority'); $this->createdAt = (string)$this->getDataProperty('createdAt'); $this->updatedAt = (string)$this->getDataProperty('updatedAt'); + $this->path = (string)$this->getDataProperty('path'); } /** @@ -200,4 +206,9 @@ public function setDirectoryId(int $directoryId): void { $this->directoryId = $directoryId; } + + public function getPath(): string + { + return $this->path; + } } diff --git a/src/CrowdinApiClient/Model/DistributionRelease.php b/src/CrowdinApiClient/Model/DistributionRelease.php index 4c587f3b..dc511c9a 100644 --- a/src/CrowdinApiClient/Model/DistributionRelease.php +++ b/src/CrowdinApiClient/Model/DistributionRelease.php @@ -34,6 +34,16 @@ class DistributionRelease extends BaseModel */ protected $date; + /** + * @var int|null + */ + protected $currentBranchId; + + /** + * @var array + */ + protected $error; + public function __construct(array $data = []) { parent::__construct($data); @@ -43,6 +53,8 @@ public function __construct(array $data = []) $this->currentLanguageId = (string)$this->getDataProperty('currentLanguageId'); $this->currentFileId = (int)$this->getDataProperty('currentFileId'); $this->date = (string)$this->getDataProperty('date'); + $this->currentBranchId = $this->nullableInt('currentBranchId'); + $this->error = (array)$this->getDataProperty('error'); } public function getStatus(): ?string @@ -94,4 +106,14 @@ public function setDate(?string $date): void { $this->date = $date; } + + public function getCurrentBranchId(): ?int + { + return $this->currentBranchId; + } + + public function getError(): array + { + return $this->error; + } } diff --git a/src/CrowdinApiClient/Model/Enterprise/Vendor.php b/src/CrowdinApiClient/Model/Enterprise/Vendor.php index 84d7fa79..0a2bda99 100644 --- a/src/CrowdinApiClient/Model/Enterprise/Vendor.php +++ b/src/CrowdinApiClient/Model/Enterprise/Vendor.php @@ -29,6 +29,11 @@ class Vendor extends BaseModel */ protected $status; + /** + * @var string + */ + protected $webUrl; + public function __construct(array $data = []) { parent::__construct($data); @@ -36,6 +41,7 @@ public function __construct(array $data = []) $this->name = (string)$this->getDataProperty('name'); $this->description = (string)$this->getDataProperty('description'); $this->status = (string)$this->getDataProperty('status'); + $this->webUrl = (string)$this->getDataProperty('webUrl'); } /** @@ -101,4 +107,9 @@ public function setStatus(string $status): void { $this->status = $status; } + + public function getWebUrl(): string + { + return $this->webUrl; + } } diff --git a/src/CrowdinApiClient/Model/Enterprise/WorkflowTemplate.php b/src/CrowdinApiClient/Model/Enterprise/WorkflowTemplate.php index d054ebc5..b5b9e8f1 100644 --- a/src/CrowdinApiClient/Model/Enterprise/WorkflowTemplate.php +++ b/src/CrowdinApiClient/Model/Enterprise/WorkflowTemplate.php @@ -35,52 +35,59 @@ class WorkflowTemplate extends BaseModel protected $isDefault; /** - * @return int + * @var array */ + protected $steps; + + /** + * @var string + */ + protected $webUrl; + + public function __construct(array $data = []) + { + parent::__construct($data); + $this->id = (int)$this->getDataProperty('id'); + $this->title = (string)$this->getDataProperty('title'); + $this->description = (string)$this->getDataProperty('description'); + $this->groupId = (int)$this->getDataProperty('groupId'); + $this->isDefault = (bool)$this->getDataProperty('isDefault'); + $this->steps = (array)$this->getDataProperty('steps'); + $this->webUrl = (string)$this->getDataProperty('webUrl'); + } + public function getId(): int { return $this->id; } - /** - * @return string - */ public function getTitle(): string { return $this->title; } - /** - * @return string - */ public function getDescription(): string { return $this->description; } - /** - * @return int - */ public function getGroupId(): int { return $this->groupId; } - /** - * @return bool - */ public function isDefault(): bool { return $this->isDefault; } - public function __construct(array $data = []) + public function getSteps(): array { - parent::__construct($data); - $this->id = (int)$this->getDataProperty('id'); - $this->title = (string)$this->getDataProperty('title'); - $this->description = (string)$this->getDataProperty('description'); - $this->groupId = (int)$this->getDataProperty('groupId'); - $this->isDefault = (bool)$this->getDataProperty('isDefault'); + return $this->steps; + } + + public function getWebUrl(): string + { + return $this->webUrl; } } diff --git a/src/CrowdinApiClient/Model/Glossary.php b/src/CrowdinApiClient/Model/Glossary.php index aa58a962..117203ca 100644 --- a/src/CrowdinApiClient/Model/Glossary.php +++ b/src/CrowdinApiClient/Model/Glossary.php @@ -62,6 +62,11 @@ class Glossary extends BaseModel */ protected $createdAt; + /** + * @var bool + */ + protected $isShared; + public function __construct(array $data = []) { parent::__construct($data); @@ -77,6 +82,7 @@ public function __construct(array $data = []) $this->projectIds = (array)$this->getDataProperty('projectIds'); $this->webUrl = (string)$this->getDataProperty('webUrl'); $this->createdAt = (string)$this->getDataProperty('createdAt'); + $this->isShared = (bool)$this->getDataProperty('isShared'); } public function getId(): int @@ -147,4 +153,14 @@ public function getCreatedAt(): string { return $this->createdAt; } + + public function isShared(): bool + { + return $this->isShared; + } + + public function setIsShared(bool $isShared): void + { + $this->isShared = $isShared; + } } diff --git a/src/CrowdinApiClient/Model/GlossaryConcept.php b/src/CrowdinApiClient/Model/GlossaryConcept.php index b15f8b03..4136b3bc 100644 --- a/src/CrowdinApiClient/Model/GlossaryConcept.php +++ b/src/CrowdinApiClient/Model/GlossaryConcept.php @@ -69,6 +69,11 @@ class GlossaryConcept extends BaseModel */ protected $updatedAt; + /** + * @var array|null + */ + protected $fields; + public function __construct(array $data) { parent::__construct($data); @@ -90,6 +95,7 @@ static function (array $languageDetails): GlossaryLanguageDetails { ); $this->createdAt = (string)$this->getDataProperty('createdAt'); $this->updatedAt = (string)$this->getDataProperty('updatedAt'); + $this->fields = $this->nullableArray('fields'); } public function getId(): int @@ -192,4 +198,9 @@ public function getUpdatedAt(): string { return $this->updatedAt; } + + public function getFields(): ?array + { + return $this->fields; + } } diff --git a/src/CrowdinApiClient/Model/Label.php b/src/CrowdinApiClient/Model/Label.php index 075bc57f..cc2881a2 100644 --- a/src/CrowdinApiClient/Model/Label.php +++ b/src/CrowdinApiClient/Model/Label.php @@ -22,6 +22,16 @@ class Label extends BaseModel */ protected $title; + /** + * @var bool|null + */ + protected $isShared; + + /** + * @var bool|null + */ + protected $isSystem; + public function __construct(array $data = []) { parent::__construct($data); @@ -29,37 +39,37 @@ public function __construct(array $data = []) $this->id = (int)$this->getDataProperty('id'); $this->projectId = (int)$this->getDataProperty('projectId'); $this->title = (string)$this->getDataProperty('title'); + $this->isShared = $this->nullableBool('isShared'); + $this->isSystem = $this->nullableBool('isSystem'); } - /** - * @return int - */ public function getId(): int { return $this->id; } - /** - * @return int - */ public function getProjectId(): int { return $this->projectId; } - /** - * @return string - */ public function getTitle(): string { return $this->title; } - /** - * @param string $title - */ public function setTitle(string $title): void { $this->title = $title; } + + public function isShared(): ?bool + { + return $this->isShared; + } + + public function isSystem(): ?bool + { + return $this->isSystem; + } } diff --git a/src/CrowdinApiClient/Model/Language.php b/src/CrowdinApiClient/Model/Language.php index 925532e2..73f54793 100644 --- a/src/CrowdinApiClient/Model/Language.php +++ b/src/CrowdinApiClient/Model/Language.php @@ -77,6 +77,11 @@ class Language extends BaseModel */ protected $dialectOf; + /** + * @var string + */ + protected $bcp47Code; + public function __construct(array $data = []) { parent::__construct($data); @@ -94,6 +99,7 @@ public function __construct(array $data = []) $this->androidCode = (string)$this->getDataProperty('androidCode'); $this->osxCode = (string)$this->getDataProperty('osxCode'); $this->osxLocale = (string)$this->getDataProperty('osxLocale'); + $this->bcp47Code = (string)$this->getDataProperty('bcp47Code'); } /** @@ -255,4 +261,9 @@ public function getOsxLocale(): string { return $this->osxLocale; } + + public function getBcp47Code(): string + { + return $this->bcp47Code; + } } diff --git a/src/CrowdinApiClient/Model/LanguageTranslation.php b/src/CrowdinApiClient/Model/LanguageTranslation.php index e726bf17..91a8d018 100644 --- a/src/CrowdinApiClient/Model/LanguageTranslation.php +++ b/src/CrowdinApiClient/Model/LanguageTranslation.php @@ -39,6 +39,41 @@ class LanguageTranslation extends BaseModel */ protected $plurals; + /** + * @var string + */ + protected $createdAt; + + /** + * @var bool + */ + protected $isPreTranslated; + + /** + * @var int|null + */ + protected $matchRate; + + /** + * @var string|null + */ + protected $matchType; + + /** + * @var string|null + */ + protected $provider; + + /** + * @var int|null + */ + protected $providerId; + + /** + * @var string + */ + protected $qaIssuesStatus; + /** * @param array $data */ @@ -57,6 +92,13 @@ public function __construct(array $data = []) ? (array)$this->getDataProperty('plurals') : null; $this->user = $this->getDataProperty('user') ? (array)$this->getDataProperty('user') : null; + $this->createdAt = (string)$this->getDataProperty('createdAt'); + $this->isPreTranslated = (bool)$this->getDataProperty('isPreTranslated'); + $this->matchRate = $this->nullableInt('matchRate'); + $this->matchType = $this->nullableString('matchType'); + $this->provider = $this->nullableString('provider'); + $this->providerId = $this->nullableInt('providerId'); + $this->qaIssuesStatus = (string)$this->getDataProperty('qaIssuesStatus'); } public function getStringId(): int @@ -108,4 +150,39 @@ public function setPlurals(array $plurals): void { $this->plurals = $plurals; } + + public function getCreatedAt(): string + { + return $this->createdAt; + } + + public function isPreTranslated(): bool + { + return $this->isPreTranslated; + } + + public function getMatchRate(): ?int + { + return $this->matchRate; + } + + public function getMatchType(): ?string + { + return $this->matchType; + } + + public function getProvider(): ?string + { + return $this->provider; + } + + public function getProviderId(): ?int + { + return $this->providerId; + } + + public function getQaIssuesStatus(): string + { + return $this->qaIssuesStatus; + } } diff --git a/src/CrowdinApiClient/Model/MachineTranslationEngine.php b/src/CrowdinApiClient/Model/MachineTranslationEngine.php index d6a0196b..6c45a60f 100644 --- a/src/CrowdinApiClient/Model/MachineTranslationEngine.php +++ b/src/CrowdinApiClient/Model/MachineTranslationEngine.php @@ -37,6 +37,31 @@ class MachineTranslationEngine extends BaseModel */ protected $projectIds; + /** + * @var array|null + */ + protected $enabledLanguageIds; + + /** + * @var array|null + */ + protected $enabledProjectIds; + + /** + * @var bool|null + */ + protected $isEnabled; + + /** + * @var array + */ + protected $supportedLanguageIds; + + /** + * @var array|null + */ + protected $supportedLanguagePairs; + /** * @param array $data */ @@ -50,6 +75,11 @@ public function __construct(array $data = []) $this->type = (string)$this->getDataProperty('type'); $this->credentials = (array)$this->getDataProperty('credentials'); $this->projectIds = (array)$this->getDataProperty('projectIds'); + $this->enabledLanguageIds = $this->nullableArray('enabledLanguageIds'); + $this->enabledProjectIds = $this->nullableArray('enabledProjectIds'); + $this->isEnabled = $this->nullableBool('isEnabled'); + $this->supportedLanguageIds = (array)$this->getDataProperty('supportedLanguageIds'); + $this->supportedLanguagePairs = $this->nullableArray('supportedLanguagePairs'); } /** @@ -123,4 +153,44 @@ public function getProjectIds(): array { return $this->projectIds; } + + public function getEnabledLanguageIds(): ?array + { + return $this->enabledLanguageIds; + } + + public function setEnabledLanguageIds(array $enabledLanguageIds): void + { + $this->enabledLanguageIds = $enabledLanguageIds; + } + + public function getEnabledProjectIds(): ?array + { + return $this->enabledProjectIds; + } + + public function setEnabledProjectIds(array $enabledProjectIds): void + { + $this->enabledProjectIds = $enabledProjectIds; + } + + public function isEnabled(): ?bool + { + return $this->isEnabled; + } + + public function setIsEnabled(bool $isEnabled): void + { + $this->isEnabled = $isEnabled; + } + + public function getSupportedLanguageIds(): array + { + return $this->supportedLanguageIds; + } + + public function getSupportedLanguagePairs(): ?array + { + return $this->supportedLanguagePairs; + } } diff --git a/src/CrowdinApiClient/Model/Progress.php b/src/CrowdinApiClient/Model/Progress.php index 6b8514f0..b0326c5c 100644 --- a/src/CrowdinApiClient/Model/Progress.php +++ b/src/CrowdinApiClient/Model/Progress.php @@ -37,6 +37,11 @@ class Progress extends BaseModel */ protected $approvalProgress; + /** + * @var array + */ + protected $qaChecksStatus; + /** * @param array $data */ @@ -50,6 +55,7 @@ public function __construct(array $data = []) $this->phrases = (array)$this->getDataProperty('phrases'); $this->translationProgress = (int)$this->getDataProperty('translationProgress'); $this->approvalProgress = (int)$this->getDataProperty('approvalProgress'); + $this->qaChecksStatus = (array)$this->getDataProperty('qaChecksStatus'); } public function getLanguage(): Language @@ -96,4 +102,9 @@ public function getApprovalProgress(): int { return $this->approvalProgress; } + + public function getQaChecksStatus(): array + { + return $this->qaChecksStatus; + } } diff --git a/src/CrowdinApiClient/Model/ProgressFile.php b/src/CrowdinApiClient/Model/ProgressFile.php index b0eb8392..f4eaac1e 100644 --- a/src/CrowdinApiClient/Model/ProgressFile.php +++ b/src/CrowdinApiClient/Model/ProgressFile.php @@ -13,28 +13,30 @@ class ProgressFile extends Progress protected $etag; /** - * @param array $data + * @var array */ + protected $qaChecksStatus; + public function __construct(array $data = []) { parent::__construct($data); $this->etag = $this->getDataProperty('eTag'); + $this->qaChecksStatus = (array)$this->getDataProperty('qaChecksStatus'); } - /** - * @return string|null - */ public function getEtag(): ?string { return $this->etag; } - /** - * @param string|null $etag - */ public function setEtag(?string $etag): void { $this->etag = $etag; } + + public function getQaChecksStatus(): array + { + return $this->qaChecksStatus; + } } diff --git a/src/CrowdinApiClient/Model/ProgressLanguage.php b/src/CrowdinApiClient/Model/ProgressLanguage.php index 548daa4f..442b327e 100644 --- a/src/CrowdinApiClient/Model/ProgressLanguage.php +++ b/src/CrowdinApiClient/Model/ProgressLanguage.php @@ -40,8 +40,15 @@ class ProgressLanguage extends BaseModel protected $etag; /** - * @param array $data + * @var int|null */ + protected $branchId; + + /** + * @var array + */ + protected $qaChecksStatus; + public function __construct(array $data = []) { parent::__construct($data); @@ -52,6 +59,8 @@ public function __construct(array $data = []) $this->approvalProgress = (int)$this->getDataProperty('approvalProgress'); $this->fileId = (int)$this->getDataProperty('fileId'); $this->etag = (string)$this->getDataProperty('eTag'); + $this->branchId = $this->nullableInt('branchId'); + $this->qaChecksStatus = (array)$this->getDataProperty('qaChecksStatus'); } /** @@ -101,4 +110,14 @@ public function getEtag(): ?string { return $this->etag; } + + public function getBranchId(): ?int + { + return $this->branchId; + } + + public function getQaChecksStatus(): array + { + return $this->qaChecksStatus; + } } diff --git a/src/CrowdinApiClient/Model/Project.php b/src/CrowdinApiClient/Model/Project.php index 740798c3..16657ce6 100644 --- a/src/CrowdinApiClient/Model/Project.php +++ b/src/CrowdinApiClient/Model/Project.php @@ -262,6 +262,151 @@ class Project extends BaseModel */ protected $fields = []; + /** + * @var array|null + */ + protected $aiPreTranslate; + + /** + * @var int|null + */ + protected $alignmentActionAiPromptId; + + /** + * @var array|null + */ + protected $assignedGlossaries; + + /** + * @var array|null + */ + protected $assignedStyleGuides; + + /** + * @var array|null + */ + protected $assignedTms; + + /** + * @var int|null + */ + protected $clientOrganizationId; + + /** + * @var int|null + */ + protected $contextReviewAiPromptId; + + /** + * @var bool|null + */ + protected $delayedWorkflowStart; + + /** + * @var int|null + */ + protected $editorSuggestionAiPromptId; + + /** + * @var bool|null + */ + protected $exportStringsThatPassedWorkflow; + + /** + * @var int|null + */ + protected $externalOrganizationId; + + /** + * @var int|null + */ + protected $externalProjectId; + + /** + * @var array|null + */ + protected $externalQaCheckIds; + + /** + * @var array|null + */ + protected $mtPreTranslate; + + /** + * @var string|null + */ + protected $publicUrl; + + /** + * @var int|null + */ + protected $qaApprovalsCount; + + /** + * @var int|null + */ + protected $qaCheckActionAiPromptId; + + /** + * @var int + */ + protected $savingsReportSettingsTemplateId; + + /** + * @var bool|null + */ + protected $showTmSuggestionsDialects; + + /** + * @var array + */ + protected $sourceLanguage; + + /** + * @var int|null + */ + protected $tagsDetection; + + /** + * @var bool|null + */ + protected $taskBasedAccessControl; + + /** + * @var array|null + */ + protected $taskReviewerIds; + + /** + * @var bool|null + */ + protected $tmApprovedSuggestionsOnly; + + /** + * @var string|null + */ + protected $tmContextType; + + /** + * @var array|null + */ + protected $tmPenalties; + + /** + * @var array|null + */ + protected $tmPreTranslate; + + /** + * @var int + */ + protected $type; + + /** + * @var string + */ + protected $webUrl; + public function __construct(array $data = []) { parent::__construct($data); @@ -309,6 +454,7 @@ public function __construct(array $data = []) $this->customQaCheckIds = (array)$this->getDataProperty('customQaCheckIds'); $this->languageMapping = (array)$this->getDataProperty('languageMapping'); $this->glossaryAccess = (bool)$this->getDataProperty('glossaryAccess'); + $this->exportTranslatedOnly = (bool)$this->getDataProperty('exportTranslatedOnly'); $this->glossaryAccessOption = $this->nullableString('glossaryAccessOption'); $this->isSuspended = (bool)$this->getDataProperty('isSuspended'); $this->normalizePlaceholder = (bool)$this->getDataProperty('normalizePlaceholder'); @@ -317,6 +463,35 @@ public function __construct(array $data = []) $this->defaultTmId = (int)$this->getDataProperty('defaultTmId'); $this->defaultGlossaryId = (int)$this->getDataProperty('defaultGlossaryId'); $this->fields = (array)$this->getDataProperty('fields'); + $this->aiPreTranslate = $this->nullableArray('aiPreTranslate'); + $this->alignmentActionAiPromptId = $this->nullableInt('alignmentActionAiPromptId'); + $this->assignedGlossaries = $this->nullableArray('assignedGlossaries'); + $this->assignedStyleGuides = $this->nullableArray('assignedStyleGuides'); + $this->assignedTms = $this->nullableArray('assignedTms'); + $this->clientOrganizationId = $this->nullableInt('clientOrganizationId'); + $this->contextReviewAiPromptId = $this->nullableInt('contextReviewAiPromptId'); + $this->delayedWorkflowStart = $this->nullableBool('delayedWorkflowStart'); + $this->editorSuggestionAiPromptId = $this->nullableInt('editorSuggestionAiPromptId'); + $this->exportStringsThatPassedWorkflow = $this->nullableBool('exportStringsThatPassedWorkflow'); + $this->externalOrganizationId = $this->nullableInt('externalOrganizationId'); + $this->externalProjectId = $this->nullableInt('externalProjectId'); + $this->externalQaCheckIds = $this->nullableArray('externalQaCheckIds'); + $this->mtPreTranslate = $this->nullableArray('mtPreTranslate'); + $this->publicUrl = $this->nullableString('publicUrl'); + $this->qaApprovalsCount = $this->nullableInt('qaApprovalsCount'); + $this->qaCheckActionAiPromptId = $this->nullableInt('qaCheckActionAiPromptId'); + $this->savingsReportSettingsTemplateId = (int)$this->getDataProperty('savingsReportSettingsTemplateId'); + $this->showTmSuggestionsDialects = $this->nullableBool('showTmSuggestionsDialects'); + $this->sourceLanguage = (array)$this->getDataProperty('sourceLanguage'); + $this->tagsDetection = $this->nullableInt('tagsDetection'); + $this->taskBasedAccessControl = $this->nullableBool('taskBasedAccessControl'); + $this->taskReviewerIds = $this->nullableArray('taskReviewerIds'); + $this->tmApprovedSuggestionsOnly = $this->nullableBool('tmApprovedSuggestionsOnly'); + $this->tmContextType = $this->nullableString('tmContextType'); + $this->tmPenalties = $this->nullableArray('tmPenalties'); + $this->tmPreTranslate = $this->nullableArray('tmPreTranslate'); + $this->type = (int)$this->getDataProperty('type'); + $this->webUrl = (string)$this->getDataProperty('webUrl'); } /** @@ -1126,4 +1301,239 @@ public function setFields(array $fields): void { $this->fields = $fields; } + + public function getAiPreTranslate(): ?array + { + return $this->aiPreTranslate; + } + + public function setAiPreTranslate(array $aiPreTranslate): void + { + $this->aiPreTranslate = $aiPreTranslate; + } + + public function getAlignmentActionAiPromptId(): ?int + { + return $this->alignmentActionAiPromptId; + } + + public function setAlignmentActionAiPromptId(int $alignmentActionAiPromptId): void + { + $this->alignmentActionAiPromptId = $alignmentActionAiPromptId; + } + + public function getAssignedGlossaries(): ?array + { + return $this->assignedGlossaries; + } + + public function setAssignedGlossaries(array $assignedGlossaries): void + { + $this->assignedGlossaries = $assignedGlossaries; + } + + public function getAssignedStyleGuides(): ?array + { + return $this->assignedStyleGuides; + } + + public function setAssignedStyleGuides(array $assignedStyleGuides): void + { + $this->assignedStyleGuides = $assignedStyleGuides; + } + + public function getAssignedTms(): ?array + { + return $this->assignedTms; + } + + public function setAssignedTms(array $assignedTms): void + { + $this->assignedTms = $assignedTms; + } + + public function getClientOrganizationId(): ?int + { + return $this->clientOrganizationId; + } + + public function getContextReviewAiPromptId(): ?int + { + return $this->contextReviewAiPromptId; + } + + public function setContextReviewAiPromptId(int $contextReviewAiPromptId): void + { + $this->contextReviewAiPromptId = $contextReviewAiPromptId; + } + + public function isDelayedWorkflowStart(): ?bool + { + return $this->delayedWorkflowStart; + } + + public function getEditorSuggestionAiPromptId(): ?int + { + return $this->editorSuggestionAiPromptId; + } + + public function setEditorSuggestionAiPromptId(int $editorSuggestionAiPromptId): void + { + $this->editorSuggestionAiPromptId = $editorSuggestionAiPromptId; + } + + public function isExportStringsThatPassedWorkflow(): ?bool + { + return $this->exportStringsThatPassedWorkflow; + } + + public function setExportStringsThatPassedWorkflow(bool $exportStringsThatPassedWorkflow): void + { + $this->exportStringsThatPassedWorkflow = $exportStringsThatPassedWorkflow; + } + + public function getExternalOrganizationId(): ?int + { + return $this->externalOrganizationId; + } + + public function getExternalProjectId(): ?int + { + return $this->externalProjectId; + } + + public function getExternalQaCheckIds(): ?array + { + return $this->externalQaCheckIds; + } + + public function getMtPreTranslate(): ?array + { + return $this->mtPreTranslate; + } + + public function setMtPreTranslate(array $mtPreTranslate): void + { + $this->mtPreTranslate = $mtPreTranslate; + } + + public function getPublicUrl(): ?string + { + return $this->publicUrl; + } + + public function getQaApprovalsCount(): ?int + { + return $this->qaApprovalsCount; + } + + public function setQaApprovalsCount(int $qaApprovalsCount): void + { + $this->qaApprovalsCount = $qaApprovalsCount; + } + + public function getQaCheckActionAiPromptId(): ?int + { + return $this->qaCheckActionAiPromptId; + } + + public function setQaCheckActionAiPromptId(int $qaCheckActionAiPromptId): void + { + $this->qaCheckActionAiPromptId = $qaCheckActionAiPromptId; + } + + public function getSavingsReportSettingsTemplateId(): int + { + return $this->savingsReportSettingsTemplateId; + } + + public function setSavingsReportSettingsTemplateId(int $savingsReportSettingsTemplateId): void + { + $this->savingsReportSettingsTemplateId = $savingsReportSettingsTemplateId; + } + + public function isShowTmSuggestionsDialects(): ?bool + { + return $this->showTmSuggestionsDialects; + } + + public function setShowTmSuggestionsDialects(bool $showTmSuggestionsDialects): void + { + $this->showTmSuggestionsDialects = $showTmSuggestionsDialects; + } + + public function getSourceLanguage(): array + { + return $this->sourceLanguage; + } + + public function getTagsDetection(): ?int + { + return $this->tagsDetection; + } + + public function isTaskBasedAccessControl(): ?bool + { + return $this->taskBasedAccessControl; + } + + public function setTaskBasedAccessControl(bool $taskBasedAccessControl): void + { + $this->taskBasedAccessControl = $taskBasedAccessControl; + } + + public function getTaskReviewerIds(): ?array + { + return $this->taskReviewerIds; + } + + public function setTaskReviewerIds(array $taskReviewerIds): void + { + $this->taskReviewerIds = $taskReviewerIds; + } + + public function isTmApprovedSuggestionsOnly(): ?bool + { + return $this->tmApprovedSuggestionsOnly; + } + + public function setTmApprovedSuggestionsOnly(bool $tmApprovedSuggestionsOnly): void + { + $this->tmApprovedSuggestionsOnly = $tmApprovedSuggestionsOnly; + } + + public function getTmContextType(): ?string + { + return $this->tmContextType; + } + + public function setTmContextType(string $tmContextType): void + { + $this->tmContextType = $tmContextType; + } + + public function getTmPenalties(): ?array + { + return $this->tmPenalties; + } + + public function getTmPreTranslate(): ?array + { + return $this->tmPreTranslate; + } + + public function setTmPreTranslate(array $tmPreTranslate): void + { + $this->tmPreTranslate = $tmPreTranslate; + } + + public function getType(): int + { + return $this->type; + } + + public function getWebUrl(): string + { + return $this->webUrl; + } } diff --git a/src/CrowdinApiClient/Model/ReportArchive.php b/src/CrowdinApiClient/Model/ReportArchive.php index 2bc797fa..621c113d 100644 --- a/src/CrowdinApiClient/Model/ReportArchive.php +++ b/src/CrowdinApiClient/Model/ReportArchive.php @@ -37,6 +37,16 @@ class ReportArchive extends BaseModel */ protected $createdAt; + /** + * @var int + */ + protected $progress; + + /** + * @var string + */ + protected $status; + public function __construct(array $data = []) { parent::__construct($data); @@ -48,6 +58,8 @@ public function __construct(array $data = []) $this->webUrl = (string)$this->getDataProperty('webUrl'); $this->scheme = (array)$this->getDataProperty('scheme'); $this->createdAt = (string)$this->getDataProperty('createdAt'); + $this->progress = (int)$this->getDataProperty('progress'); + $this->status = (string)$this->getDataProperty('status'); } public function getId(): int @@ -124,4 +136,14 @@ public function setCreatedAt(string $createdAt): void { $this->createdAt = $createdAt; } + + public function getProgress(): int + { + return $this->progress; + } + + public function getStatus(): string + { + return $this->status; + } } diff --git a/src/CrowdinApiClient/Model/SourceString.php b/src/CrowdinApiClient/Model/SourceString.php index 8d17a16d..c6b0dcc7 100644 --- a/src/CrowdinApiClient/Model/SourceString.php +++ b/src/CrowdinApiClient/Model/SourceString.php @@ -109,6 +109,11 @@ class SourceString extends BaseModel */ protected $fields = []; + /** + * @var string + */ + protected $webUrl; + public function __construct(array $data = []) { parent::__construct($data); @@ -139,6 +144,7 @@ public function __construct(array $data = []) $this->isDuplicate = (bool)$this->getDataProperty('isDuplicate'); $this->masterStringId = $this->getDataProperty('masterStringId'); $this->fields = (array)$this->getDataProperty('fields'); + $this->webUrl = (string)$this->getDataProperty('webUrl'); } /** @@ -376,4 +382,9 @@ public function setFields(array $fields): void { $this->fields = $fields; } + + public function getWebUrl(): string + { + return $this->webUrl; + } } diff --git a/src/CrowdinApiClient/Model/StringComment.php b/src/CrowdinApiClient/Model/StringComment.php index 9e362447..e0098667 100644 --- a/src/CrowdinApiClient/Model/StringComment.php +++ b/src/CrowdinApiClient/Model/StringComment.php @@ -82,6 +82,36 @@ class StringComment extends BaseModel */ protected $attachments; + /** + * @var array|null + */ + protected $file; + + /** + * @var int|null + */ + protected $fileId; + + /** + * @var bool|null + */ + protected $isShared; + + /** + * @var int + */ + protected $projectId; + + /** + * @var array|null + */ + protected $resolverOrganization; + + /** + * @var array|null + */ + protected $senderOrganization; + public function __construct(array $data = []) { parent::__construct($data); @@ -100,6 +130,12 @@ public function __construct(array $data = []) $this->resolvedAt = (string)$this->getDataProperty('resolvedAt'); $this->createdAt = (string)$this->getDataProperty('createdAt'); $this->attachments = (array)$this->getDataProperty('attachments'); + $this->file = $this->nullableArray('file'); + $this->fileId = $this->nullableInt('fileId'); + $this->isShared = $this->nullableBool('isShared'); + $this->projectId = (int)$this->getDataProperty('projectId'); + $this->resolverOrganization = $this->nullableArray('resolverOrganization'); + $this->senderOrganization = $this->nullableArray('senderOrganization'); } /** @@ -341,4 +377,34 @@ public function setAttachments(array $attachments): void { $this->attachments = $attachments; } + + public function getFile(): ?array + { + return $this->file; + } + + public function getFileId(): ?int + { + return $this->fileId; + } + + public function isShared(): ?bool + { + return $this->isShared; + } + + public function getProjectId(): int + { + return $this->projectId; + } + + public function getResolverOrganization(): ?array + { + return $this->resolverOrganization; + } + + public function getSenderOrganization(): ?array + { + return $this->senderOrganization; + } } diff --git a/src/CrowdinApiClient/Model/StringTranslation.php b/src/CrowdinApiClient/Model/StringTranslation.php index e7d0921d..c2a2d3c7 100644 --- a/src/CrowdinApiClient/Model/StringTranslation.php +++ b/src/CrowdinApiClient/Model/StringTranslation.php @@ -52,6 +52,41 @@ class StringTranslation extends BaseModel */ protected $createdAt; + /** + * @var bool + */ + protected $isPreTranslated; + + /** + * @var int|null + */ + protected $matchRate; + + /** + * @var string|null + */ + protected $matchType; + + /** + * @var string|null + */ + protected $provider; + + /** + * @var int|null + */ + protected $providerId; + + /** + * @var string|null + */ + protected $url; + + /** + * @var int|null + */ + protected $workflowStepId; + public function __construct(array $data = []) { parent::__construct($data); @@ -67,9 +102,17 @@ public function __construct(array $data = []) ? (string)$this->getDataProperty('languageId') : null; $this->text = (string)$this->getDataProperty('text'); + $this->createdAt = (string)$this->getDataProperty('createdAt'); $this->pluralCategoryName = (string)$this->getDataProperty('pluralCategoryName'); $this->user = (array)$this->getDataProperty('user'); $this->rating = (int)$this->getDataProperty('rating'); + $this->isPreTranslated = (bool)$this->getDataProperty('isPreTranslated'); + $this->matchRate = $this->nullableInt('matchRate'); + $this->matchType = $this->nullableString('matchType'); + $this->provider = $this->nullableString('provider'); + $this->providerId = $this->nullableInt('providerId'); + $this->url = $this->nullableString('url'); + $this->workflowStepId = $this->nullableInt('workflowStepId'); } /** @@ -185,4 +228,39 @@ public function setCreatedAt(string $createdAt): void { $this->createdAt = $createdAt; } + + public function isPreTranslated(): bool + { + return $this->isPreTranslated; + } + + public function getMatchRate(): ?int + { + return $this->matchRate; + } + + public function getMatchType(): ?string + { + return $this->matchType; + } + + public function getProvider(): ?string + { + return $this->provider; + } + + public function getProviderId(): ?int + { + return $this->providerId; + } + + public function getUrl(): ?string + { + return $this->url; + } + + public function getWorkflowStepId(): ?int + { + return $this->workflowStepId; + } } diff --git a/src/CrowdinApiClient/Model/StringTranslationApproval.php b/src/CrowdinApiClient/Model/StringTranslationApproval.php index 3d3d9867..08d78b29 100644 --- a/src/CrowdinApiClient/Model/StringTranslationApproval.php +++ b/src/CrowdinApiClient/Model/StringTranslationApproval.php @@ -42,6 +42,11 @@ class StringTranslationApproval extends BaseModel */ protected $createdAt; + /** + * @var int + */ + protected $fileId; + public function __construct(array $data = []) { parent::__construct($data); @@ -53,6 +58,7 @@ public function __construct(array $data = []) $this->languageId = (string)$this->getDataProperty('languageId'); $this->workflowStepId = (int)$this->getDataProperty('workflowStepId'); $this->createdAt = (string)$this->getDataProperty('createdAt'); + $this->fileId = (int)$this->getDataProperty('fileId'); } /** @@ -166,4 +172,9 @@ public function setCreatedAt(string $createdAt): void { $this->createdAt = $createdAt; } + + public function getFileId(): int + { + return $this->fileId; + } } diff --git a/src/CrowdinApiClient/Model/StyleGuide.php b/src/CrowdinApiClient/Model/StyleGuide.php index 5428957a..abd1cafb 100644 --- a/src/CrowdinApiClient/Model/StyleGuide.php +++ b/src/CrowdinApiClient/Model/StyleGuide.php @@ -39,6 +39,9 @@ class StyleGuide extends BaseModel /** @var string */ protected $updatedAt; + /** @var int|null */ + protected $groupId; + public function __construct(array $data = []) { parent::__construct($data); @@ -54,6 +57,7 @@ public function __construct(array $data = []) $this->downloadLink = (string)$this->getDataProperty('downloadLink'); $this->createdAt = (string)$this->getDataProperty('createdAt'); $this->updatedAt = (string)$this->getDataProperty('updatedAt'); + $this->groupId = $this->nullableInt('groupId'); } public function getId(): int @@ -141,4 +145,14 @@ public function setIsShared(bool $isShared): void { $this->isShared = $isShared; } + + public function getGroupId(): ?int + { + return $this->groupId; + } + + public function setGroupId(int $groupId): void + { + $this->groupId = $groupId; + } } diff --git a/src/CrowdinApiClient/Model/Task.php b/src/CrowdinApiClient/Model/Task.php index cc6b4bd2..db6585c0 100644 --- a/src/CrowdinApiClient/Model/Task.php +++ b/src/CrowdinApiClient/Model/Task.php @@ -233,6 +233,21 @@ class Task extends BaseModel */ protected $hash; + /** + * @var array|null + */ + protected $branchIds; + + /** + * @var int + */ + protected $originalWordsCount; + + /** + * @var array|null + */ + protected $syncScope; + public function __construct(array $data = []) { parent::__construct($data); @@ -289,6 +304,9 @@ public function __construct(array $data = []) $this->vendor = $this->getDataProperty('vendor') ? (string)$this->getDataProperty('vendor') : null; $this->filesCount = (int)$this->getDataProperty('filesCount'); $this->fileIds = (array)$this->getDataProperty('fileIds'); + $this->branchIds = $this->nullableArray('branchIds'); + $this->originalWordsCount = (int)$this->getDataProperty('originalWordsCount'); + $this->syncScope = $this->nullableArray('syncScope'); // User Task $this->isArchived = (bool)$this->getDataProperty('isArchived'); @@ -740,4 +758,19 @@ public function getHash(): ?string { return $this->hash; } + + public function getBranchIds(): ?array + { + return $this->branchIds; + } + + public function getOriginalWordsCount(): int + { + return $this->originalWordsCount; + } + + public function getSyncScope(): ?array + { + return $this->syncScope; + } } diff --git a/src/CrowdinApiClient/Model/Term.php b/src/CrowdinApiClient/Model/Term.php index 2a2b5990..269cacba 100644 --- a/src/CrowdinApiClient/Model/Term.php +++ b/src/CrowdinApiClient/Model/Term.php @@ -87,6 +87,11 @@ class Term extends BaseModel */ protected $updatedAt; + /** + * @var array|null + */ + protected $fields; + /** * @param array $data */ @@ -110,6 +115,7 @@ public function __construct(array $data = []) $this->lemma = (string)$this->getDataProperty('lemma'); $this->createdAt = (string)$this->getDataProperty('createdAt'); $this->updatedAt = (string)$this->getDataProperty('updatedAt'); + $this->fields = $this->nullableArray('fields'); } /** @@ -270,4 +276,14 @@ public function getUpdatedAt(): string { return $this->updatedAt; } + + public function getFields(): ?array + { + return $this->fields; + } + + public function setFields(array $fields): void + { + $this->fields = $fields; + } } diff --git a/src/CrowdinApiClient/Model/TranslationMemory.php b/src/CrowdinApiClient/Model/TranslationMemory.php index 090ed09c..69b40220 100644 --- a/src/CrowdinApiClient/Model/TranslationMemory.php +++ b/src/CrowdinApiClient/Model/TranslationMemory.php @@ -47,6 +47,26 @@ class TranslationMemory extends BaseModel */ protected $createdAt; + /** + * @var bool + */ + protected $isShared; + + /** + * @var string|null + */ + protected $languageId; + + /** + * @var int|null + */ + protected $userId; + + /** + * @var string + */ + protected $webUrl; + /** * @param array $data */ @@ -62,6 +82,10 @@ public function __construct(array $data = []) $this->defaultProjectIds = $this->getDataProperty('defaultProjectIds'); $this->projectIds = $this->getDataProperty('projectIds'); $this->createdAt = $this->getDataProperty('createdAt'); + $this->isShared = (bool)$this->getDataProperty('isShared'); + $this->languageId = $this->nullableString('languageId'); + $this->userId = $this->nullableInt('userId'); + $this->webUrl = (string)$this->getDataProperty('webUrl'); } /** @@ -143,4 +167,34 @@ public function setCreatedAt(string $createdAt): void { $this->createdAt = $createdAt; } + + public function isShared(): bool + { + return $this->isShared; + } + + public function setIsShared(bool $isShared): void + { + $this->isShared = $isShared; + } + + public function getLanguageId(): ?string + { + return $this->languageId; + } + + public function setLanguageId(string $languageId): void + { + $this->languageId = $languageId; + } + + public function getUserId(): ?int + { + return $this->userId; + } + + public function getWebUrl(): string + { + return $this->webUrl; + } } diff --git a/src/CrowdinApiClient/Model/TranslationProjectBuild.php b/src/CrowdinApiClient/Model/TranslationProjectBuild.php index a15bd0e0..1e179dab 100644 --- a/src/CrowdinApiClient/Model/TranslationProjectBuild.php +++ b/src/CrowdinApiClient/Model/TranslationProjectBuild.php @@ -32,6 +32,26 @@ class TranslationProjectBuild extends BaseModel */ protected $attributes; + /** + * @var string|null + */ + protected $createdAt; + + /** + * @var array + */ + protected $error; + + /** + * @var string|null + */ + protected $finishedAt; + + /** + * @var string|null + */ + protected $updatedAt; + public function __construct(array $data = []) { parent::__construct($data); @@ -41,6 +61,10 @@ public function __construct(array $data = []) $this->status = (string)$this->getDataProperty('status'); $this->progress = (int)$this->getDataProperty('progress'); $this->attributes = (array)$this->getDataProperty('attributes'); + $this->createdAt = $this->nullableString('createdAt'); + $this->error = (array)$this->getDataProperty('error'); + $this->finishedAt = $this->nullableString('finishedAt'); + $this->updatedAt = $this->nullableString('updatedAt'); } public function getId(): int @@ -67,4 +91,24 @@ public function getAttributes(): array { return $this->attributes; } + + public function getCreatedAt(): ?string + { + return $this->createdAt; + } + + public function getError(): array + { + return $this->error; + } + + public function getFinishedAt(): ?string + { + return $this->finishedAt; + } + + public function getUpdatedAt(): ?string + { + return $this->updatedAt; + } } diff --git a/tests/CrowdinApiClient/Model/AiReportTest.php b/tests/CrowdinApiClient/Model/AiReportTest.php new file mode 100644 index 00000000..2f3621f0 --- /dev/null +++ b/tests/CrowdinApiClient/Model/AiReportTest.php @@ -0,0 +1,27 @@ + 'value', + ]); + + $this->assertSame('value', $model->getEta()); + } + + public function testAdditionalFieldsWhenMissing(): void + { + $model = new AiReport([]); + + $this->assertNull($model->getEta()); + } +} diff --git a/tests/CrowdinApiClient/Model/AiSettingsTest.php b/tests/CrowdinApiClient/Model/AiSettingsTest.php new file mode 100644 index 00000000..b3041bef --- /dev/null +++ b/tests/CrowdinApiClient/Model/AiSettingsTest.php @@ -0,0 +1,61 @@ + 7, + 'dailyCostLimit' => 1.5, + 'isLimitingActive' => true, + 'monthlyCostLimit' => 1.5, + 'perUserOverrides' => ['key' => 'value'], + 'userDailyCostLimit' => 1.5, + 'userMonthlyCostLimit' => 1.5, + ]); + + $this->assertSame(7, $model->getAlignmentActionAiPromptId()); + $this->assertSame(1.5, $model->getDailyCostLimit()); + $this->assertSame(true, $model->isLimitingActive()); + $this->assertSame(1.5, $model->getMonthlyCostLimit()); + $this->assertSame(['key' => 'value'], $model->getPerUserOverrides()); + $this->assertSame(1.5, $model->getUserDailyCostLimit()); + $this->assertSame(1.5, $model->getUserMonthlyCostLimit()); + } + + public function testAdditionalFieldsWhenMissing(): void + { + $model = new AiSettings([]); + + $this->assertNull($model->getAlignmentActionAiPromptId()); + $this->assertNull($model->getDailyCostLimit()); + $this->assertSame(false, $model->isLimitingActive()); + $this->assertNull($model->getMonthlyCostLimit()); + $this->assertSame([], $model->getPerUserOverrides()); + $this->assertNull($model->getUserDailyCostLimit()); + $this->assertNull($model->getUserMonthlyCostLimit()); + } + + public function testSetAdditionalFields(): void + { + $model = new AiSettings([]); + $model->setAlignmentActionAiPromptId(7); + $model->setDailyCostLimit(1.5); + $model->setMonthlyCostLimit(1.5); + $model->setUserDailyCostLimit(1.5); + $model->setUserMonthlyCostLimit(1.5); + + $this->assertSame(7, $model->getAlignmentActionAiPromptId()); + $this->assertSame(1.5, $model->getDailyCostLimit()); + $this->assertSame(1.5, $model->getMonthlyCostLimit()); + $this->assertSame(1.5, $model->getUserDailyCostLimit()); + $this->assertSame(1.5, $model->getUserMonthlyCostLimit()); + } +} diff --git a/tests/CrowdinApiClient/Model/BundleTest.php b/tests/CrowdinApiClient/Model/BundleTest.php index 53d316b4..a891cd28 100644 --- a/tests/CrowdinApiClient/Model/BundleTest.php +++ b/tests/CrowdinApiClient/Model/BundleTest.php @@ -96,4 +96,39 @@ public function checkData(): void $this->assertEquals($this->data['createdAt'], $this->bundle->getCreatedAt()); $this->assertEquals($this->data['updatedAt'], $this->bundle->getUpdatedAt()); } + + public function testLoadAdditionalFields(): void + { + $model = new Bundle([ + 'excludeLabelMatchRule' => 'value', + 'labelMatchRule' => 'value', + 'languageIds' => ['key' => 'value'], + 'sourceLanguageExportPattern' => 'value', + ]); + + $this->assertSame('value', $model->getExcludeLabelMatchRule()); + $this->assertSame('value', $model->getLabelMatchRule()); + $this->assertSame(['key' => 'value'], $model->getLanguageIds()); + $this->assertSame('value', $model->getSourceLanguageExportPattern()); + } + + public function testAdditionalFieldsWhenMissing(): void + { + $model = new Bundle([]); + + $this->assertNull($model->getExcludeLabelMatchRule()); + $this->assertNull($model->getLabelMatchRule()); + $this->assertNull($model->getLanguageIds()); + $this->assertSame('', $model->getSourceLanguageExportPattern()); + } + + public function testSetAdditionalFields(): void + { + $model = new Bundle([]); + $model->setExcludeLabelMatchRule('value'); + $model->setLabelMatchRule('value'); + + $this->assertSame('value', $model->getExcludeLabelMatchRule()); + $this->assertSame('value', $model->getLabelMatchRule()); + } } diff --git a/tests/CrowdinApiClient/Model/DirectoryTest.php b/tests/CrowdinApiClient/Model/DirectoryTest.php index 3d89aaf8..801dd93a 100644 --- a/tests/CrowdinApiClient/Model/DirectoryTest.php +++ b/tests/CrowdinApiClient/Model/DirectoryTest.php @@ -65,4 +65,20 @@ public function checkData() $this->assertEquals($this->data['createdAt'], $this->directory->getCreatedAt()); $this->assertEquals($this->data['updatedAt'], $this->directory->getUpdatedAt()); } + + public function testLoadAdditionalFields(): void + { + $model = new Directory([ + 'path' => 'value', + ]); + + $this->assertSame('value', $model->getPath()); + } + + public function testAdditionalFieldsWhenMissing(): void + { + $model = new Directory([]); + + $this->assertSame('', $model->getPath()); + } } diff --git a/tests/CrowdinApiClient/Model/DistributionReleaseTest.php b/tests/CrowdinApiClient/Model/DistributionReleaseTest.php index 12a988bb..8978e0d2 100644 --- a/tests/CrowdinApiClient/Model/DistributionReleaseTest.php +++ b/tests/CrowdinApiClient/Model/DistributionReleaseTest.php @@ -53,4 +53,23 @@ public function checkData() $this->assertEquals($this->data['currentFileId'], $this->distributionRelease->getCurrentFileId()); $this->assertEquals($this->data['date'], $this->distributionRelease->getDate()); } + + public function testLoadAdditionalFields(): void + { + $model = new DistributionRelease([ + 'currentBranchId' => 7, + 'error' => ['key' => 'value'], + ]); + + $this->assertSame(7, $model->getCurrentBranchId()); + $this->assertSame(['key' => 'value'], $model->getError()); + } + + public function testAdditionalFieldsWhenMissing(): void + { + $model = new DistributionRelease([]); + + $this->assertNull($model->getCurrentBranchId()); + $this->assertSame([], $model->getError()); + } } diff --git a/tests/CrowdinApiClient/Model/Enterprise/VendorTest.php b/tests/CrowdinApiClient/Model/Enterprise/VendorTest.php index 4f8594c8..ac232515 100644 --- a/tests/CrowdinApiClient/Model/Enterprise/VendorTest.php +++ b/tests/CrowdinApiClient/Model/Enterprise/VendorTest.php @@ -39,4 +39,20 @@ public function checkData(): void $this->assertEquals($this->data['description'], $this->vendor->getDescription()); $this->assertEquals($this->data['status'], $this->vendor->getStatus()); } + + public function testLoadAdditionalFields(): void + { + $model = new Vendor([ + 'webUrl' => 'value', + ]); + + $this->assertSame('value', $model->getWebUrl()); + } + + public function testAdditionalFieldsWhenMissing(): void + { + $model = new Vendor([]); + + $this->assertSame('', $model->getWebUrl()); + } } diff --git a/tests/CrowdinApiClient/Model/Enterprise/WorkflowTemplateTest.php b/tests/CrowdinApiClient/Model/Enterprise/WorkflowTemplateTest.php index e527d70f..25bb3413 100644 --- a/tests/CrowdinApiClient/Model/Enterprise/WorkflowTemplateTest.php +++ b/tests/CrowdinApiClient/Model/Enterprise/WorkflowTemplateTest.php @@ -26,4 +26,23 @@ public function testLoadData(): void $this->assertEquals($this->data['groupId'], $this->workflowTemplate->getGroupId()); $this->assertEquals($this->data['isDefault'], $this->workflowTemplate->isDefault()); } + + public function testLoadAdditionalFields(): void + { + $model = new WorkflowTemplate([ + 'steps' => ['key' => 'value'], + 'webUrl' => 'value', + ]); + + $this->assertSame(['key' => 'value'], $model->getSteps()); + $this->assertSame('value', $model->getWebUrl()); + } + + public function testAdditionalFieldsWhenMissing(): void + { + $model = new WorkflowTemplate([]); + + $this->assertSame([], $model->getSteps()); + $this->assertSame('', $model->getWebUrl()); + } } diff --git a/tests/CrowdinApiClient/Model/GlossaryConceptTest.php b/tests/CrowdinApiClient/Model/GlossaryConceptTest.php index 95901b8d..b3f86396 100644 --- a/tests/CrowdinApiClient/Model/GlossaryConceptTest.php +++ b/tests/CrowdinApiClient/Model/GlossaryConceptTest.php @@ -94,4 +94,20 @@ public function checkData(): void $this->assertEquals($this->data['createdAt'], $this->glossaryConcept->getCreatedAt()); $this->assertEquals($this->data['updatedAt'], $this->glossaryConcept->getUpdatedAt()); } + + public function testLoadAdditionalFields(): void + { + $model = new GlossaryConcept([ + 'fields' => ['key' => 'value'], + ]); + + $this->assertSame(['key' => 'value'], $model->getFields()); + } + + public function testAdditionalFieldsWhenMissing(): void + { + $model = new GlossaryConcept([]); + + $this->assertNull($model->getFields()); + } } diff --git a/tests/CrowdinApiClient/Model/GlossaryTest.php b/tests/CrowdinApiClient/Model/GlossaryTest.php index 4d82d83e..0732caef 100644 --- a/tests/CrowdinApiClient/Model/GlossaryTest.php +++ b/tests/CrowdinApiClient/Model/GlossaryTest.php @@ -57,4 +57,28 @@ public function checkData(): void $this->assertEquals($this->data['webUrl'], $this->glossary->getWebUrl()); $this->assertEquals($this->data['createdAt'], $this->glossary->getCreatedAt()); } + + public function testLoadAdditionalFields(): void + { + $model = new Glossary([ + 'isShared' => true, + ]); + + $this->assertSame(true, $model->isShared()); + } + + public function testAdditionalFieldsWhenMissing(): void + { + $model = new Glossary([]); + + $this->assertSame(false, $model->isShared()); + } + + public function testSetAdditionalFields(): void + { + $model = new Glossary([]); + $model->setIsShared(true); + + $this->assertSame(true, $model->isShared()); + } } diff --git a/tests/CrowdinApiClient/Model/LabelTest.php b/tests/CrowdinApiClient/Model/LabelTest.php index baf612ae..465eca99 100644 --- a/tests/CrowdinApiClient/Model/LabelTest.php +++ b/tests/CrowdinApiClient/Model/LabelTest.php @@ -39,4 +39,23 @@ public function checkData() $this->assertEquals($this->data['id'], $this->label->getId()); $this->assertEquals($this->data['title'], $this->label->getTitle()); } + + public function testLoadAdditionalFields(): void + { + $model = new Label([ + 'isShared' => true, + 'isSystem' => true, + ]); + + $this->assertSame(true, $model->isShared()); + $this->assertSame(true, $model->isSystem()); + } + + public function testAdditionalFieldsWhenMissing(): void + { + $model = new Label([]); + + $this->assertNull($model->isShared()); + $this->assertNull($model->isSystem()); + } } diff --git a/tests/CrowdinApiClient/Model/LanguageTest.php b/tests/CrowdinApiClient/Model/LanguageTest.php index 23e0d231..4589ef2d 100644 --- a/tests/CrowdinApiClient/Model/LanguageTest.php +++ b/tests/CrowdinApiClient/Model/LanguageTest.php @@ -79,4 +79,20 @@ public function checkData() $this->assertEquals($this->data['textDirection'], $this->language->getTextDirection()); $this->assertEquals($this->data['dialectOf'], $this->language->getDialectOf()); } + + public function testLoadAdditionalFields(): void + { + $model = new Language([ + 'bcp47Code' => 'value', + ]); + + $this->assertSame('value', $model->getBcp47Code()); + } + + public function testAdditionalFieldsWhenMissing(): void + { + $model = new Language([]); + + $this->assertSame('', $model->getBcp47Code()); + } } diff --git a/tests/CrowdinApiClient/Model/LanguageTranslationTest.php b/tests/CrowdinApiClient/Model/LanguageTranslationTest.php new file mode 100644 index 00000000..a420859b --- /dev/null +++ b/tests/CrowdinApiClient/Model/LanguageTranslationTest.php @@ -0,0 +1,45 @@ + 'value', + 'isPreTranslated' => true, + 'matchRate' => 7, + 'matchType' => 'value', + 'provider' => 'value', + 'providerId' => 7, + 'qaIssuesStatus' => 'value', + ]); + + $this->assertSame('value', $model->getCreatedAt()); + $this->assertSame(true, $model->isPreTranslated()); + $this->assertSame(7, $model->getMatchRate()); + $this->assertSame('value', $model->getMatchType()); + $this->assertSame('value', $model->getProvider()); + $this->assertSame(7, $model->getProviderId()); + $this->assertSame('value', $model->getQaIssuesStatus()); + } + + public function testAdditionalFieldsWhenMissing(): void + { + $model = new LanguageTranslation([]); + + $this->assertSame('', $model->getCreatedAt()); + $this->assertSame(false, $model->isPreTranslated()); + $this->assertNull($model->getMatchRate()); + $this->assertNull($model->getMatchType()); + $this->assertNull($model->getProvider()); + $this->assertNull($model->getProviderId()); + $this->assertSame('', $model->getQaIssuesStatus()); + } +} diff --git a/tests/CrowdinApiClient/Model/MachineTranslationEngineTest.php b/tests/CrowdinApiClient/Model/MachineTranslationEngineTest.php index 14489dc4..0f6e6e2d 100644 --- a/tests/CrowdinApiClient/Model/MachineTranslationEngineTest.php +++ b/tests/CrowdinApiClient/Model/MachineTranslationEngineTest.php @@ -48,4 +48,44 @@ public function checkData() $this->assertEquals($this->data['credentials'], $this->machineTranslationEngine->getCredentials()); $this->assertEquals($this->data['projectIds'], $this->machineTranslationEngine->getProjectIds()); } + + public function testLoadAdditionalFields(): void + { + $model = new MachineTranslationEngine([ + 'enabledLanguageIds' => ['key' => 'value'], + 'enabledProjectIds' => ['key' => 'value'], + 'isEnabled' => true, + 'supportedLanguageIds' => ['key' => 'value'], + 'supportedLanguagePairs' => ['key' => 'value'], + ]); + + $this->assertSame(['key' => 'value'], $model->getEnabledLanguageIds()); + $this->assertSame(['key' => 'value'], $model->getEnabledProjectIds()); + $this->assertSame(true, $model->isEnabled()); + $this->assertSame(['key' => 'value'], $model->getSupportedLanguageIds()); + $this->assertSame(['key' => 'value'], $model->getSupportedLanguagePairs()); + } + + public function testAdditionalFieldsWhenMissing(): void + { + $model = new MachineTranslationEngine([]); + + $this->assertNull($model->getEnabledLanguageIds()); + $this->assertNull($model->getEnabledProjectIds()); + $this->assertNull($model->isEnabled()); + $this->assertSame([], $model->getSupportedLanguageIds()); + $this->assertNull($model->getSupportedLanguagePairs()); + } + + public function testSetAdditionalFields(): void + { + $model = new MachineTranslationEngine([]); + $model->setEnabledLanguageIds(['key' => 'value']); + $model->setEnabledProjectIds(['key' => 'value']); + $model->setIsEnabled(true); + + $this->assertSame(['key' => 'value'], $model->getEnabledLanguageIds()); + $this->assertSame(['key' => 'value'], $model->getEnabledProjectIds()); + $this->assertSame(true, $model->isEnabled()); + } } diff --git a/tests/CrowdinApiClient/Model/ProgressFileTest.php b/tests/CrowdinApiClient/Model/ProgressFileTest.php index eb829082..86dd413e 100644 --- a/tests/CrowdinApiClient/Model/ProgressFileTest.php +++ b/tests/CrowdinApiClient/Model/ProgressFileTest.php @@ -61,4 +61,21 @@ public function checkData(): void $this->assertEquals($this->data['approvalProgress'], $this->progress->getApprovalProgress()); $this->assertEquals($this->data['eTag'], $this->progress->getEtag()); } + + public function testLoadAdditionalFields(): void + { + $model = new ProgressFile([ + 'language' => [], + 'qaChecksStatus' => ['key' => 'value'], + ]); + + $this->assertSame(['key' => 'value'], $model->getQaChecksStatus()); + } + + public function testAdditionalFieldsWhenMissing(): void + { + $model = new ProgressFile(['language' => []]); + + $this->assertSame([], $model->getQaChecksStatus()); + } } diff --git a/tests/CrowdinApiClient/Model/ProgressLanguageTest.php b/tests/CrowdinApiClient/Model/ProgressLanguageTest.php index 2e51b5c2..5136962c 100644 --- a/tests/CrowdinApiClient/Model/ProgressLanguageTest.php +++ b/tests/CrowdinApiClient/Model/ProgressLanguageTest.php @@ -46,4 +46,23 @@ public function checkData(): void $this->assertEquals($this->data['fileId'], $this->progress->getFileId()); $this->assertEquals($this->data['eTag'], $this->progress->getEtag()); } + + public function testLoadAdditionalFields(): void + { + $model = new ProgressLanguage([ + 'branchId' => 7, + 'qaChecksStatus' => ['key' => 'value'], + ]); + + $this->assertSame(7, $model->getBranchId()); + $this->assertSame(['key' => 'value'], $model->getQaChecksStatus()); + } + + public function testAdditionalFieldsWhenMissing(): void + { + $model = new ProgressLanguage([]); + + $this->assertNull($model->getBranchId()); + $this->assertSame([], $model->getQaChecksStatus()); + } } diff --git a/tests/CrowdinApiClient/Model/ProgressTest.php b/tests/CrowdinApiClient/Model/ProgressTest.php index 274bcc85..80e45b7c 100644 --- a/tests/CrowdinApiClient/Model/ProgressTest.php +++ b/tests/CrowdinApiClient/Model/ProgressTest.php @@ -65,4 +65,21 @@ public function checkData(): void $this->assertEquals($this->data['translationProgress'], $this->progress->getTranslationProgress()); $this->assertEquals($this->data['approvalProgress'], $this->progress->getApprovalProgress()); } + + public function testLoadAdditionalFields(): void + { + $model = new Progress([ + 'language' => [], + 'qaChecksStatus' => ['key' => 'value'], + ]); + + $this->assertSame(['key' => 'value'], $model->getQaChecksStatus()); + } + + public function testAdditionalFieldsWhenMissing(): void + { + $model = new Progress(['language' => []]); + + $this->assertSame([], $model->getQaChecksStatus()); + } } diff --git a/tests/CrowdinApiClient/Model/ProjectTest.php b/tests/CrowdinApiClient/Model/ProjectTest.php index c10090c6..50d1676c 100644 --- a/tests/CrowdinApiClient/Model/ProjectTest.php +++ b/tests/CrowdinApiClient/Model/ProjectTest.php @@ -236,4 +236,152 @@ public function testLoadDataWithoutGlossaryAccessOption(): void $this->assertNull((new Project($data))->getGlossaryAccessOption()); } + + public function testLoadAdditionalFields(): void + { + $model = new Project([ + 'aiPreTranslate' => ['key' => 'value'], + 'alignmentActionAiPromptId' => 7, + 'assignedGlossaries' => ['key' => 'value'], + 'assignedStyleGuides' => ['key' => 'value'], + 'assignedTms' => ['key' => 'value'], + 'clientOrganizationId' => 7, + 'contextReviewAiPromptId' => 7, + 'delayedWorkflowStart' => true, + 'editorSuggestionAiPromptId' => 7, + 'exportStringsThatPassedWorkflow' => true, + 'externalOrganizationId' => 7, + 'externalProjectId' => 7, + 'externalQaCheckIds' => ['key' => 'value'], + 'mtPreTranslate' => ['key' => 'value'], + 'publicUrl' => 'value', + 'qaApprovalsCount' => 7, + 'qaCheckActionAiPromptId' => 7, + 'savingsReportSettingsTemplateId' => 7, + 'showTmSuggestionsDialects' => true, + 'sourceLanguage' => ['key' => 'value'], + 'tagsDetection' => 7, + 'taskBasedAccessControl' => true, + 'taskReviewerIds' => ['key' => 'value'], + 'tmApprovedSuggestionsOnly' => true, + 'tmContextType' => 'value', + 'tmPenalties' => ['key' => 'value'], + 'tmPreTranslate' => ['key' => 'value'], + 'type' => 7, + 'webUrl' => 'value', + ]); + + $this->assertSame(['key' => 'value'], $model->getAiPreTranslate()); + $this->assertSame(7, $model->getAlignmentActionAiPromptId()); + $this->assertSame(['key' => 'value'], $model->getAssignedGlossaries()); + $this->assertSame(['key' => 'value'], $model->getAssignedStyleGuides()); + $this->assertSame(['key' => 'value'], $model->getAssignedTms()); + $this->assertSame(7, $model->getClientOrganizationId()); + $this->assertSame(7, $model->getContextReviewAiPromptId()); + $this->assertSame(true, $model->isDelayedWorkflowStart()); + $this->assertSame(7, $model->getEditorSuggestionAiPromptId()); + $this->assertSame(true, $model->isExportStringsThatPassedWorkflow()); + $this->assertSame(7, $model->getExternalOrganizationId()); + $this->assertSame(7, $model->getExternalProjectId()); + $this->assertSame(['key' => 'value'], $model->getExternalQaCheckIds()); + $this->assertSame(['key' => 'value'], $model->getMtPreTranslate()); + $this->assertSame('value', $model->getPublicUrl()); + $this->assertSame(7, $model->getQaApprovalsCount()); + $this->assertSame(7, $model->getQaCheckActionAiPromptId()); + $this->assertSame(7, $model->getSavingsReportSettingsTemplateId()); + $this->assertSame(true, $model->isShowTmSuggestionsDialects()); + $this->assertSame(['key' => 'value'], $model->getSourceLanguage()); + $this->assertSame(7, $model->getTagsDetection()); + $this->assertSame(true, $model->isTaskBasedAccessControl()); + $this->assertSame(['key' => 'value'], $model->getTaskReviewerIds()); + $this->assertSame(true, $model->isTmApprovedSuggestionsOnly()); + $this->assertSame('value', $model->getTmContextType()); + $this->assertSame(['key' => 'value'], $model->getTmPenalties()); + $this->assertSame(['key' => 'value'], $model->getTmPreTranslate()); + $this->assertSame(7, $model->getType()); + $this->assertSame('value', $model->getWebUrl()); + } + + public function testAdditionalFieldsWhenMissing(): void + { + $model = new Project([]); + + $this->assertNull($model->getAiPreTranslate()); + $this->assertNull($model->getAlignmentActionAiPromptId()); + $this->assertNull($model->getAssignedGlossaries()); + $this->assertNull($model->getAssignedStyleGuides()); + $this->assertNull($model->getAssignedTms()); + $this->assertNull($model->getClientOrganizationId()); + $this->assertNull($model->getContextReviewAiPromptId()); + $this->assertNull($model->isDelayedWorkflowStart()); + $this->assertNull($model->getEditorSuggestionAiPromptId()); + $this->assertNull($model->isExportStringsThatPassedWorkflow()); + $this->assertNull($model->getExternalOrganizationId()); + $this->assertNull($model->getExternalProjectId()); + $this->assertNull($model->getExternalQaCheckIds()); + $this->assertNull($model->getMtPreTranslate()); + $this->assertNull($model->getPublicUrl()); + $this->assertNull($model->getQaApprovalsCount()); + $this->assertNull($model->getQaCheckActionAiPromptId()); + $this->assertSame(0, $model->getSavingsReportSettingsTemplateId()); + $this->assertNull($model->isShowTmSuggestionsDialects()); + $this->assertSame([], $model->getSourceLanguage()); + $this->assertNull($model->getTagsDetection()); + $this->assertNull($model->isTaskBasedAccessControl()); + $this->assertNull($model->getTaskReviewerIds()); + $this->assertNull($model->isTmApprovedSuggestionsOnly()); + $this->assertNull($model->getTmContextType()); + $this->assertNull($model->getTmPenalties()); + $this->assertNull($model->getTmPreTranslate()); + $this->assertSame(0, $model->getType()); + $this->assertSame('', $model->getWebUrl()); + } + + public function testSetAdditionalFields(): void + { + $model = new Project([]); + $model->setAiPreTranslate(['key' => 'value']); + $model->setAlignmentActionAiPromptId(7); + $model->setAssignedGlossaries(['key' => 'value']); + $model->setAssignedStyleGuides(['key' => 'value']); + $model->setAssignedTms(['key' => 'value']); + $model->setContextReviewAiPromptId(7); + $model->setEditorSuggestionAiPromptId(7); + $model->setExportStringsThatPassedWorkflow(true); + $model->setMtPreTranslate(['key' => 'value']); + $model->setQaApprovalsCount(7); + $model->setQaCheckActionAiPromptId(7); + $model->setSavingsReportSettingsTemplateId(7); + $model->setShowTmSuggestionsDialects(true); + $model->setTaskBasedAccessControl(true); + $model->setTaskReviewerIds(['key' => 'value']); + $model->setTmApprovedSuggestionsOnly(true); + $model->setTmContextType('value'); + $model->setTmPreTranslate(['key' => 'value']); + + $this->assertSame(['key' => 'value'], $model->getAiPreTranslate()); + $this->assertSame(7, $model->getAlignmentActionAiPromptId()); + $this->assertSame(['key' => 'value'], $model->getAssignedGlossaries()); + $this->assertSame(['key' => 'value'], $model->getAssignedStyleGuides()); + $this->assertSame(['key' => 'value'], $model->getAssignedTms()); + $this->assertSame(7, $model->getContextReviewAiPromptId()); + $this->assertSame(7, $model->getEditorSuggestionAiPromptId()); + $this->assertSame(true, $model->isExportStringsThatPassedWorkflow()); + $this->assertSame(['key' => 'value'], $model->getMtPreTranslate()); + $this->assertSame(7, $model->getQaApprovalsCount()); + $this->assertSame(7, $model->getQaCheckActionAiPromptId()); + $this->assertSame(7, $model->getSavingsReportSettingsTemplateId()); + $this->assertSame(true, $model->isShowTmSuggestionsDialects()); + $this->assertSame(true, $model->isTaskBasedAccessControl()); + $this->assertSame(['key' => 'value'], $model->getTaskReviewerIds()); + $this->assertSame(true, $model->isTmApprovedSuggestionsOnly()); + $this->assertSame('value', $model->getTmContextType()); + $this->assertSame(['key' => 'value'], $model->getTmPreTranslate()); + } + + public function testExportTranslatedOnlyIsHydrated(): void + { + $this->assertTrue((new Project(['exportTranslatedOnly' => true]))->isExportTranslatedOnly()); + $this->assertFalse((new Project([]))->isExportTranslatedOnly()); + } } diff --git a/tests/CrowdinApiClient/Model/ReportArchiveTest.php b/tests/CrowdinApiClient/Model/ReportArchiveTest.php new file mode 100644 index 00000000..1582dd49 --- /dev/null +++ b/tests/CrowdinApiClient/Model/ReportArchiveTest.php @@ -0,0 +1,30 @@ + 7, + 'status' => 'value', + ]); + + $this->assertSame(7, $model->getProgress()); + $this->assertSame('value', $model->getStatus()); + } + + public function testAdditionalFieldsWhenMissing(): void + { + $model = new ReportArchive([]); + + $this->assertSame(0, $model->getProgress()); + $this->assertSame('', $model->getStatus()); + } +} diff --git a/tests/CrowdinApiClient/Model/SourceStringTest.php b/tests/CrowdinApiClient/Model/SourceStringTest.php index f4b26974..fd2d44e9 100644 --- a/tests/CrowdinApiClient/Model/SourceStringTest.php +++ b/tests/CrowdinApiClient/Model/SourceStringTest.php @@ -166,4 +166,20 @@ public function testSetTextException($originalText, $updatedText, string $expect $sourceString = new SourceString(['text' => $originalText]); $sourceString->setText($updatedText); } + + public function testLoadAdditionalFields(): void + { + $model = new SourceString([ + 'webUrl' => 'value', + ]); + + $this->assertSame('value', $model->getWebUrl()); + } + + public function testAdditionalFieldsWhenMissing(): void + { + $model = new SourceString([]); + + $this->assertSame('', $model->getWebUrl()); + } } diff --git a/tests/CrowdinApiClient/Model/StringCommentTest.php b/tests/CrowdinApiClient/Model/StringCommentTest.php index bc6ce2d0..17df2546 100644 --- a/tests/CrowdinApiClient/Model/StringCommentTest.php +++ b/tests/CrowdinApiClient/Model/StringCommentTest.php @@ -106,4 +106,35 @@ public function checkData() $this->assertEquals($this->data['createdAt'], $this->stringComment->getCreatedAt()); $this->assertEquals($this->data['attachments'], $this->stringComment->getAttachments()); } + + public function testLoadAdditionalFields(): void + { + $model = new StringComment([ + 'file' => ['key' => 'value'], + 'fileId' => 7, + 'isShared' => true, + 'projectId' => 7, + 'resolverOrganization' => ['key' => 'value'], + 'senderOrganization' => ['key' => 'value'], + ]); + + $this->assertSame(['key' => 'value'], $model->getFile()); + $this->assertSame(7, $model->getFileId()); + $this->assertSame(true, $model->isShared()); + $this->assertSame(7, $model->getProjectId()); + $this->assertSame(['key' => 'value'], $model->getResolverOrganization()); + $this->assertSame(['key' => 'value'], $model->getSenderOrganization()); + } + + public function testAdditionalFieldsWhenMissing(): void + { + $model = new StringComment([]); + + $this->assertNull($model->getFile()); + $this->assertNull($model->getFileId()); + $this->assertNull($model->isShared()); + $this->assertSame(0, $model->getProjectId()); + $this->assertNull($model->getResolverOrganization()); + $this->assertNull($model->getSenderOrganization()); + } } diff --git a/tests/CrowdinApiClient/Model/StringTranslationApprovalTest.php b/tests/CrowdinApiClient/Model/StringTranslationApprovalTest.php index b2858349..83b6cda3 100644 --- a/tests/CrowdinApiClient/Model/StringTranslationApprovalTest.php +++ b/tests/CrowdinApiClient/Model/StringTranslationApprovalTest.php @@ -52,4 +52,20 @@ public function checkData() $this->assertEquals($this->data['workflowStepId'], $this->stringTranslationApproval->getWorkflowStepId()); $this->assertEquals($this->data['createdAt'], $this->stringTranslationApproval->getCreatedAt()); } + + public function testLoadAdditionalFields(): void + { + $model = new StringTranslationApproval([ + 'fileId' => 7, + ]); + + $this->assertSame(7, $model->getFileId()); + } + + public function testAdditionalFieldsWhenMissing(): void + { + $model = new StringTranslationApproval([]); + + $this->assertSame(0, $model->getFileId()); + } } diff --git a/tests/CrowdinApiClient/Model/StringTranslationTest.php b/tests/CrowdinApiClient/Model/StringTranslationTest.php index f5a71cd1..0480ea85 100644 --- a/tests/CrowdinApiClient/Model/StringTranslationTest.php +++ b/tests/CrowdinApiClient/Model/StringTranslationTest.php @@ -62,4 +62,45 @@ public function checkData() $this->assertEquals($this->data['rating'], $this->stringTranslation->getRating()); $this->assertEquals($this->data['createdAt'], $this->stringTranslation->getCreatedAt()); } + + public function testLoadAdditionalFields(): void + { + $model = new StringTranslation([ + 'isPreTranslated' => true, + 'matchRate' => 7, + 'matchType' => 'value', + 'provider' => 'value', + 'providerId' => 7, + 'url' => 'value', + 'workflowStepId' => 7, + ]); + + $this->assertSame(true, $model->isPreTranslated()); + $this->assertSame(7, $model->getMatchRate()); + $this->assertSame('value', $model->getMatchType()); + $this->assertSame('value', $model->getProvider()); + $this->assertSame(7, $model->getProviderId()); + $this->assertSame('value', $model->getUrl()); + $this->assertSame(7, $model->getWorkflowStepId()); + } + + public function testAdditionalFieldsWhenMissing(): void + { + $model = new StringTranslation([]); + + $this->assertSame(false, $model->isPreTranslated()); + $this->assertNull($model->getMatchRate()); + $this->assertNull($model->getMatchType()); + $this->assertNull($model->getProvider()); + $this->assertNull($model->getProviderId()); + $this->assertNull($model->getUrl()); + $this->assertNull($model->getWorkflowStepId()); + } + + public function testCreatedAtIsHydrated(): void + { + $model = new StringTranslation(['createdAt' => '2026-01-01T00:00:00+00:00']); + + $this->assertSame('2026-01-01T00:00:00+00:00', $model->getCreatedAt()); + } } diff --git a/tests/CrowdinApiClient/Model/StyleGuideTest.php b/tests/CrowdinApiClient/Model/StyleGuideTest.php index c5095bb1..53a651b8 100644 --- a/tests/CrowdinApiClient/Model/StyleGuideTest.php +++ b/tests/CrowdinApiClient/Model/StyleGuideTest.php @@ -82,4 +82,28 @@ public function testSetData(): void $this->assertEquals([1, 2, 3], $styleGuide->getProjectIds()); $this->assertTrue($styleGuide->isShared()); } + + public function testLoadAdditionalFields(): void + { + $model = new StyleGuide([ + 'groupId' => 7, + ]); + + $this->assertSame(7, $model->getGroupId()); + } + + public function testAdditionalFieldsWhenMissing(): void + { + $model = new StyleGuide([]); + + $this->assertNull($model->getGroupId()); + } + + public function testSetAdditionalFields(): void + { + $model = new StyleGuide([]); + $model->setGroupId(7); + + $this->assertSame(7, $model->getGroupId()); + } } diff --git a/tests/CrowdinApiClient/Model/TaskTest.php b/tests/CrowdinApiClient/Model/TaskTest.php index 7962f280..e1bc1bc3 100644 --- a/tests/CrowdinApiClient/Model/TaskTest.php +++ b/tests/CrowdinApiClient/Model/TaskTest.php @@ -229,4 +229,26 @@ public function testSetData(): void $this->assertEquals($vendor, $task->getVendor()); $this->assertEquals($isArchived, $task->isArchived()); } + + public function testLoadAdditionalFields(): void + { + $model = new Task([ + 'branchIds' => ['key' => 'value'], + 'originalWordsCount' => 7, + 'syncScope' => ['key' => 'value'], + ]); + + $this->assertSame(['key' => 'value'], $model->getBranchIds()); + $this->assertSame(7, $model->getOriginalWordsCount()); + $this->assertSame(['key' => 'value'], $model->getSyncScope()); + } + + public function testAdditionalFieldsWhenMissing(): void + { + $model = new Task([]); + + $this->assertNull($model->getBranchIds()); + $this->assertSame(0, $model->getOriginalWordsCount()); + $this->assertNull($model->getSyncScope()); + } } diff --git a/tests/CrowdinApiClient/Model/TermTest.php b/tests/CrowdinApiClient/Model/TermTest.php index 820c0bfc..bf8202b5 100644 --- a/tests/CrowdinApiClient/Model/TermTest.php +++ b/tests/CrowdinApiClient/Model/TermTest.php @@ -84,4 +84,28 @@ public function checkData(): void $this->assertEquals($this->data['createdAt'], $this->term->getCreatedAt()); $this->assertEquals($this->data['updatedAt'], $this->term->getUpdatedAt()); } + + public function testLoadAdditionalFields(): void + { + $model = new Term([ + 'fields' => ['key' => 'value'], + ]); + + $this->assertSame(['key' => 'value'], $model->getFields()); + } + + public function testAdditionalFieldsWhenMissing(): void + { + $model = new Term([]); + + $this->assertNull($model->getFields()); + } + + public function testSetAdditionalFields(): void + { + $model = new Term([]); + $model->setFields(['key' => 'value']); + + $this->assertSame(['key' => 'value'], $model->getFields()); + } } diff --git a/tests/CrowdinApiClient/Model/TranslationMemoryTest.php b/tests/CrowdinApiClient/Model/TranslationMemoryTest.php index a7931258..0dafd0cc 100644 --- a/tests/CrowdinApiClient/Model/TranslationMemoryTest.php +++ b/tests/CrowdinApiClient/Model/TranslationMemoryTest.php @@ -53,4 +53,39 @@ public function checkData() $this->assertEquals($this->data['projectIds'], $this->translationMemory->getProjectIds()); $this->assertEquals($this->data['createdAt'], $this->translationMemory->getCreatedAt()); } + + public function testLoadAdditionalFields(): void + { + $model = new TranslationMemory([ + 'isShared' => true, + 'languageId' => 'value', + 'userId' => 7, + 'webUrl' => 'value', + ]); + + $this->assertSame(true, $model->isShared()); + $this->assertSame('value', $model->getLanguageId()); + $this->assertSame(7, $model->getUserId()); + $this->assertSame('value', $model->getWebUrl()); + } + + public function testAdditionalFieldsWhenMissing(): void + { + $model = new TranslationMemory([]); + + $this->assertSame(false, $model->isShared()); + $this->assertNull($model->getLanguageId()); + $this->assertNull($model->getUserId()); + $this->assertSame('', $model->getWebUrl()); + } + + public function testSetAdditionalFields(): void + { + $model = new TranslationMemory([]); + $model->setIsShared(true); + $model->setLanguageId('value'); + + $this->assertSame(true, $model->isShared()); + $this->assertSame('value', $model->getLanguageId()); + } } diff --git a/tests/CrowdinApiClient/Model/TranslationProjectBuildTest.php b/tests/CrowdinApiClient/Model/TranslationProjectBuildTest.php index cdbdf995..8de36e5e 100644 --- a/tests/CrowdinApiClient/Model/TranslationProjectBuildTest.php +++ b/tests/CrowdinApiClient/Model/TranslationProjectBuildTest.php @@ -29,4 +29,29 @@ public function testLoadData(): void $this->assertEquals($this->data['progress'], $translationProjectBuild->getProgress()); $this->assertEquals($this->data['attributes'], $translationProjectBuild->getAttributes()); } + + public function testLoadAdditionalFields(): void + { + $model = new TranslationProjectBuild([ + 'createdAt' => 'value', + 'error' => ['key' => 'value'], + 'finishedAt' => 'value', + 'updatedAt' => 'value', + ]); + + $this->assertSame('value', $model->getCreatedAt()); + $this->assertSame(['key' => 'value'], $model->getError()); + $this->assertSame('value', $model->getFinishedAt()); + $this->assertSame('value', $model->getUpdatedAt()); + } + + public function testAdditionalFieldsWhenMissing(): void + { + $model = new TranslationProjectBuild([]); + + $this->assertNull($model->getCreatedAt()); + $this->assertSame([], $model->getError()); + $this->assertNull($model->getFinishedAt()); + $this->assertNull($model->getUpdatedAt()); + } } From 8274f3e4d83529f0dd74b656ae4828f0c604b969 Mon Sep 17 00:00:00 2001 From: Bohdan Bobko <172895427+bhdnb@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:16:46 +0300 Subject: [PATCH 9/9] chore: mark remaining deprecated member getters and prompt params --- src/CrowdinApiClient/Api/AiApi.php | 2 +- src/CrowdinApiClient/Api/Enterprise/AiApi.php | 4 ++-- .../Model/Enterprise/ProjectTeamMemberResource.php | 6 ++++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/CrowdinApiClient/Api/AiApi.php b/src/CrowdinApiClient/Api/AiApi.php index 030b84b1..4e0b182e 100644 --- a/src/CrowdinApiClient/Api/AiApi.php +++ b/src/CrowdinApiClient/Api/AiApi.php @@ -165,7 +165,7 @@ public function listPrompts(int $userId, array $params = []): ModelCollection * array $data[config] required
* integer $data[aiProviderId]
* string $data[aiModelId]
- * boolean $data[isEnabled]
+ * boolean $data[isEnabled] Deprecated
* array $data[enabledProjectIds] * @return AiPrompt|null */ diff --git a/src/CrowdinApiClient/Api/Enterprise/AiApi.php b/src/CrowdinApiClient/Api/Enterprise/AiApi.php index b23e2871..83dc84d1 100644 --- a/src/CrowdinApiClient/Api/Enterprise/AiApi.php +++ b/src/CrowdinApiClient/Api/Enterprise/AiApi.php @@ -151,11 +151,11 @@ public function listPrompts(array $params = []): ModelCollection * * @param array $data * string $data[name] required
- * string $data[action] required
+ * string $data[action] required. Note: "assist" is deprecated
* array $data[config] required
* integer $data[aiProviderId]
* string $data[aiModelId]
- * boolean $data[isEnabled]
+ * boolean $data[isEnabled] Deprecated
* array $data[enabledProjectIds] * @return AiPrompt|null */ diff --git a/src/CrowdinApiClient/Model/Enterprise/ProjectTeamMemberResource.php b/src/CrowdinApiClient/Model/Enterprise/ProjectTeamMemberResource.php index ee071796..6119d81f 100644 --- a/src/CrowdinApiClient/Model/Enterprise/ProjectTeamMemberResource.php +++ b/src/CrowdinApiClient/Model/Enterprise/ProjectTeamMemberResource.php @@ -136,11 +136,17 @@ public function getManagerOfGroup(): array return $this->managerOfGroup; } + /** + * @deprecated Deprecated by the Crowdin Enterprise API. Use getRoles() instead. + */ public function isAccessToAllWorkflowSteps(): bool { return $this->accessToAllWorkflowSteps; } + /** + * @deprecated Deprecated by the Crowdin Enterprise API. Use getRoles() instead. + */ public function getPermissions(): array { return $this->permissions;