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;