From 7784b94ef804a49c217250906310af2b58f46047 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 13 Nov 2025 15:17:16 +0000
Subject: [PATCH 1/3] Initial plan
From 21c204aa9cee76df6096b0fa55021858c86f8b0e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 13 Nov 2025 15:28:50 +0000
Subject: [PATCH 2/3] Add batch operation endpoints for String Comments,
Approvals, and Translations
Co-authored-by: andrii-bodnar <29282228+andrii-bodnar@users.noreply.github.com>
---
src/CrowdinApiClient/Api/StringCommentApi.php | 18 +++++
.../Api/StringTranslationApi.php | 36 ++++++++++
.../Api/StringCommentApiTest.php | 63 ++++++++++++++++
.../Api/StringTranslationApiTest.php | 72 +++++++++++++++++++
4 files changed, 189 insertions(+)
diff --git a/src/CrowdinApiClient/Api/StringCommentApi.php b/src/CrowdinApiClient/Api/StringCommentApi.php
index d8afbcb4..f29d86e0 100644
--- a/src/CrowdinApiClient/Api/StringCommentApi.php
+++ b/src/CrowdinApiClient/Api/StringCommentApi.php
@@ -86,4 +86,22 @@ public function delete(int $projectId, int $stringCommentId)
$path = sprintf('projects/%d/comments/%d', $projectId, $stringCommentId);
return $this->_delete($path);
}
+
+ /**
+ * String Comment Batch Operations
+ * @link https://developer.crowdin.com/api/v2/#operation/api.projects.comments.batchPatch API Documentation
+ * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.comments.batchPatch API Documentation Enterprise
+ *
+ * @param int $projectId
+ * @param array $data
+ * string $data[op] required Patch operation to perform (replace, test)
+ * string $data[path] required A JSON Pointer as defined in RFC 6901
+ * value $data[value] required The value to be used within the operation (string, int, bool, or object)
+ * @return mixed
+ */
+ public function batchOperations(int $projectId, array $data)
+ {
+ $path = sprintf('projects/%d/comments', $projectId);
+ return $this->_patch($path, StringComment::class, $data);
+ }
}
diff --git a/src/CrowdinApiClient/Api/StringTranslationApi.php b/src/CrowdinApiClient/Api/StringTranslationApi.php
index 8c663981..3344d7a1 100644
--- a/src/CrowdinApiClient/Api/StringTranslationApi.php
+++ b/src/CrowdinApiClient/Api/StringTranslationApi.php
@@ -284,4 +284,40 @@ public function deleteVote(int $projectId, int $voteId)
$path = sprintf('projects/%d/votes/%d', $projectId, $voteId);
return $this->_delete($path);
}
+
+ /**
+ * Approval Batch Operations
+ * @link https://developer.crowdin.com/api/v2/#operation/api.projects.approvals.patch API Documentation
+ * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.approvals.patch API Documentation Enterprise
+ *
+ * @param int $projectId
+ * @param array $data
+ * string $data[op] required Patch operation to perform (replace, test)
+ * string $data[path] required A JSON Pointer as defined in RFC 6901
+ * value $data[value] required The value to be used within the operation (string, int, bool, or object)
+ * @return mixed
+ */
+ public function batchOperationsForApprovals(int $projectId, array $data)
+ {
+ $path = sprintf('projects/%d/approvals', $projectId);
+ return $this->_patch($path, StringTranslationApproval::class, $data);
+ }
+
+ /**
+ * Translation Batch Operations
+ * @link https://developer.crowdin.com/api/v2/#operation/api.projects.translations.patch API Documentation
+ * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.translations.patch API Documentation Enterprise
+ *
+ * @param int $projectId
+ * @param array $data
+ * string $data[op] required Patch operation to perform (replace, test)
+ * string $data[path] required A JSON Pointer as defined in RFC 6901
+ * value $data[value] required The value to be used within the operation (string, int, bool, or object)
+ * @return mixed
+ */
+ public function batchOperationsForTranslations(int $projectId, array $data)
+ {
+ $path = sprintf('projects/%d/translations', $projectId);
+ return $this->_patch($path, StringTranslation::class, $data);
+ }
}
diff --git a/tests/CrowdinApiClient/Api/StringCommentApiTest.php b/tests/CrowdinApiClient/Api/StringCommentApiTest.php
index 34ec1dc0..475ea0e5 100644
--- a/tests/CrowdinApiClient/Api/StringCommentApiTest.php
+++ b/tests/CrowdinApiClient/Api/StringCommentApiTest.php
@@ -218,4 +218,67 @@ public function testDelete()
$this->mockRequestDelete('/projects/1/comments/1');
$this->crowdin->stringComment->delete(1, 1);
}
+
+ public function testBatchOperations()
+ {
+ $this->mockRequest([
+ 'path' => '/projects/1/comments',
+ 'method' => 'patch',
+ 'response' => '{
+ "data": [
+ {
+ "data": {
+ "id": 2,
+ "text": "Updated comment text",
+ "userId": 6,
+ "stringId": 742,
+ "user": {
+ "id": 12,
+ "username": "john_smith",
+ "fullName": "John Smith",
+ "avatarUrl": ""
+ },
+ "string": {
+ "id": 123,
+ "text": "HTML page example",
+ "type": "text",
+ "hasPlurals": false,
+ "isIcu": false,
+ "context": "Document Title\\r\\nXPath: /html/head/title",
+ "fileId": 22
+ },
+ "languageId": "bg",
+ "type": "issue",
+ "issueType": "source_mistake",
+ "issueStatus": "resolved",
+ "resolverId": 6,
+ "resolver": {
+ "id": 12,
+ "username": "john_smith",
+ "fullName": "John Smith",
+ "avatarUrl": ""
+ },
+ "resolvedAt": "2019-09-20T11:05:24+00:00",
+ "createdAt": "2019-09-20T11:05:24+00:00"
+ }
+ }
+ ]
+ }'
+ ]);
+
+ $batchResult = $this->crowdin->stringComment->batchOperations(1, [
+ [
+ 'op' => 'replace',
+ 'path' => '/2/text',
+ 'value' => 'Updated comment text'
+ ],
+ [
+ 'op' => 'replace',
+ 'path' => '/2/issueStatus',
+ 'value' => 'resolved'
+ ]
+ ]);
+
+ $this->assertInstanceOf(StringComment::class, $batchResult);
+ }
}
diff --git a/tests/CrowdinApiClient/Api/StringTranslationApiTest.php b/tests/CrowdinApiClient/Api/StringTranslationApiTest.php
index 9bfdebab..8aa92130 100644
--- a/tests/CrowdinApiClient/Api/StringTranslationApiTest.php
+++ b/tests/CrowdinApiClient/Api/StringTranslationApiTest.php
@@ -218,4 +218,76 @@ public function testListLanguageTranslations(): void
$this->assertEquals(7815, $stringTranslations[1]->getStringId());
$this->assertEquals(9011, $stringTranslations[2]->getStringId());
}
+
+ public function testBatchOperationsForApprovals(): void
+ {
+ $this->mockRequest([
+ 'path' => '/projects/2/approvals',
+ 'method' => 'patch',
+ 'response' => json_encode([
+ 'data' => [
+ [
+ 'data' => [
+ 'id' => 12,
+ 'user' => [
+ 'id' => 12,
+ 'username' => 'john_smith',
+ 'fullName' => 'John Smith',
+ 'userType' => 'translator'
+ ],
+ 'translationId' => 190695,
+ 'stringId' => 35434,
+ 'languageId' => 'uk',
+ 'workflowStepId' => 10,
+ 'createdAt' => '2019-09-20T11:05:24+00:00'
+ ]
+ ]
+ ]
+ ])
+ ]);
+
+ $batchResult = $this->crowdin->stringTranslation->batchOperationsForApprovals(2, [
+ [
+ 'op' => 'replace',
+ 'path' => '/12/workflowStepId',
+ 'value' => 10
+ ]
+ ]);
+
+ $this->assertInstanceOf(\CrowdinApiClient\Model\StringTranslationApproval::class, $batchResult);
+ }
+
+ public function testBatchOperationsForTranslations(): void
+ {
+ $this->mockRequest([
+ 'path' => '/projects/2/translations',
+ 'method' => 'patch',
+ 'response' => json_encode([
+ 'data' => [
+ [
+ 'data' => [
+ 'id' => 190695,
+ 'text' => 'Updated translation text',
+ 'pluralCategoryName' => 'few',
+ 'user' => [
+ 'id' => 19,
+ 'login' => 'john_doe',
+ ],
+ 'rating' => 10,
+ ]
+ ]
+ ]
+ ])
+ ]);
+
+ $batchResult = $this->crowdin->stringTranslation->batchOperationsForTranslations(2, [
+ [
+ 'op' => 'replace',
+ 'path' => '/190695/text',
+ 'value' => 'Updated translation text'
+ ]
+ ]);
+
+ $this->assertInstanceOf(StringTranslation::class, $batchResult);
+ }
}
From ea3230570b289fcf617c9af1d576832b03e8fede Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 13 Nov 2025 15:42:08 +0000
Subject: [PATCH 3/3] Rename batch operation methods for better naming
consistency
Co-authored-by: andrii-bodnar <29282228+andrii-bodnar@users.noreply.github.com>
---
src/CrowdinApiClient/Api/StringTranslationApi.php | 4 ++--
tests/CrowdinApiClient/Api/StringTranslationApiTest.php | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/CrowdinApiClient/Api/StringTranslationApi.php b/src/CrowdinApiClient/Api/StringTranslationApi.php
index 3344d7a1..ace80f6f 100644
--- a/src/CrowdinApiClient/Api/StringTranslationApi.php
+++ b/src/CrowdinApiClient/Api/StringTranslationApi.php
@@ -297,7 +297,7 @@ public function deleteVote(int $projectId, int $voteId)
* value $data[value] required The value to be used within the operation (string, int, bool, or object)
* @return mixed
*/
- public function batchOperationsForApprovals(int $projectId, array $data)
+ public function approvalsBatchOperations(int $projectId, array $data)
{
$path = sprintf('projects/%d/approvals', $projectId);
return $this->_patch($path, StringTranslationApproval::class, $data);
@@ -315,7 +315,7 @@ public function batchOperationsForApprovals(int $projectId, array $data)
* value $data[value] required The value to be used within the operation (string, int, bool, or object)
* @return mixed
*/
- public function batchOperationsForTranslations(int $projectId, array $data)
+ public function translationsBatchOperations(int $projectId, array $data)
{
$path = sprintf('projects/%d/translations', $projectId);
return $this->_patch($path, StringTranslation::class, $data);
diff --git a/tests/CrowdinApiClient/Api/StringTranslationApiTest.php b/tests/CrowdinApiClient/Api/StringTranslationApiTest.php
index 8aa92130..9456340f 100644
--- a/tests/CrowdinApiClient/Api/StringTranslationApiTest.php
+++ b/tests/CrowdinApiClient/Api/StringTranslationApiTest.php
@@ -246,7 +246,7 @@ public function testBatchOperationsForApprovals(): void
])
]);
- $batchResult = $this->crowdin->stringTranslation->batchOperationsForApprovals(2, [
+ $batchResult = $this->crowdin->stringTranslation->approvalsBatchOperations(2, [
[
'op' => 'replace',
'path' => '/12/workflowStepId',
@@ -280,7 +280,7 @@ public function testBatchOperationsForTranslations(): void
])
]);
- $batchResult = $this->crowdin->stringTranslation->batchOperationsForTranslations(2, [
+ $batchResult = $this->crowdin->stringTranslation->translationsBatchOperations(2, [
[
'op' => 'replace',
'path' => '/190695/text',