From ee73e7a471d6883c9daf7b559d666614fc4e196c Mon Sep 17 00:00:00 2001 From: Andrii Bodnar <29282228+andrii-bodnar@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:46:49 +0200 Subject: [PATCH 1/2] feat: add support for update options in string batch operations and edit string methods (#645) --- src/sourceStrings/index.ts | 10 +++- tests/sourceStrings/api.test.ts | 90 +++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/src/sourceStrings/index.ts b/src/sourceStrings/index.ts index 4ad9c65e1..d5ba49da7 100644 --- a/src/sourceStrings/index.ts +++ b/src/sourceStrings/index.ts @@ -134,13 +134,16 @@ export class SourceStrings extends CrowdinApi { /** * @param projectId project identifier * @param request request body + * @param query query params * @see https://developer.crowdin.com/api/v2/#operation/api.projects.strings.batchPatch */ stringBatchOperations( projectId: number, request: PatchRequest[], + query?: { updateOption?: SourceStringsModel.UpdateOption }, ): Promise> { - const url = `${this.url}/projects/${projectId}/strings`; + let url = `${this.url}/projects/${projectId}/strings`; + url = this.addQueryParam(url, 'updateOption', query?.updateOption); return this.patch(url, request, this.defaultConfig()); } @@ -174,14 +177,17 @@ export class SourceStrings extends CrowdinApi { * @param projectId project identifier * @param stringId string identifier * @param request request body + * @param query query params * @see https://developer.crowdin.com/api/v2/#operation/api.projects.strings.patch */ editString( projectId: number, stringId: number, request: PatchRequest[], + query?: { updateOption?: SourceStringsModel.UpdateOption }, ): Promise> { - const url = `${this.url}/projects/${projectId}/strings/${stringId}`; + let url = `${this.url}/projects/${projectId}/strings/${stringId}`; + url = this.addQueryParam(url, 'updateOption', query?.updateOption); return this.patch(url, request, this.defaultConfig()); } } diff --git a/tests/sourceStrings/api.test.ts b/tests/sourceStrings/api.test.ts index 5670908ee..878eb0052 100644 --- a/tests/sourceStrings/api.test.ts +++ b/tests/sourceStrings/api.test.ts @@ -16,6 +16,7 @@ describe('Source Strings API', () => { const branchId = 1212; const storageId = 2332; const fileId = 111; + const updateOption = 'keep_translations'; const limit = 25; @@ -147,6 +148,56 @@ describe('Source Strings API', () => { }, }, ) + .reply(200, { + data: { + id: stringId, + text: stringText, + }, + }) + .patch( + `/projects/${projectId}/strings?updateOption=${updateOption}`, + [ + { + value: stringText, + op: 'replace', + path: `/${stringId}/text`, + }, + ], + { + reqheaders: { + Authorization: `Bearer ${api.token}`, + }, + }, + ) + .reply(200, { + data: [ + { + data: { + id: stringId, + text: stringText, + }, + }, + ], + pagination: { + offset: 0, + limit: limit, + }, + }) + .patch( + `/projects/${projectId}/strings/${stringId}?updateOption=${updateOption}`, + [ + { + value: stringText, + op: 'replace', + path: '/text', + }, + ], + { + reqheaders: { + Authorization: `Bearer ${api.token}`, + }, + }, + ) .reply(200, { data: { id: stringId, @@ -204,6 +255,26 @@ describe('Source Strings API', () => { expect(string.data.text).toBe(stringText); }); + it('String batch operations with update option', async () => { + const strings = await api.stringBatchOperations( + projectId, + [ + { + op: 'replace', + path: `/${stringId}/text`, + value: stringText, + }, + ], + { + updateOption, + }, + ); + expect(strings.data.length).toBe(1); + const string = strings.data[0]; + expect(string.data.id).toBe(stringId); + expect(string.data.text).toBe(stringText); + }); + it('Get string', async () => { const string = await api.getString(projectId, stringId); expect(string.data.id).toBe(stringId); @@ -225,4 +296,23 @@ describe('Source Strings API', () => { expect(string.data.id).toBe(stringId); expect(string.data.text).toBe(stringText); }); + + it('Edit string with update option', async () => { + const string = await api.editString( + projectId, + stringId, + [ + { + op: 'replace', + path: '/text', + value: stringText, + }, + ], + { + updateOption, + }, + ); + expect(string.data.id).toBe(stringId); + expect(string.data.text).toBe(stringText); + }); }); From 50774eca89ba699519218753111e2f5d0aa39f92 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 18:02:05 +0200 Subject: [PATCH 2/2] feat: add AI Gateway provider methods (#646) --- src/ai/index.ts | 148 +++++++++++++++++++++++++++++++++++++++++++ tests/ai/api.test.ts | 130 +++++++++++++++++++++++++++++++++++++ 2 files changed, 278 insertions(+) diff --git a/src/ai/index.ts b/src/ai/index.ts index 5cd566635..0d10de123 100644 --- a/src/ai/index.ts +++ b/src/ai/index.ts @@ -520,6 +520,76 @@ export class Ai extends CrowdinApi { return this.get(url, this.defaultConfig()); } + /** + * @param aiProviderId ai Provider identifier + * @param path raw provider API path after `/gateway/` + * @see https://support.crowdin.com/developer/enterprise/api/v2/#tag/AI-Gateway/operation/api.ai.providers.gateway.enterprise.get + */ + organizationAiGatewayGet(aiProviderId: number, path: string): Promise> { + const url = `${this.url}/ai/providers/${aiProviderId}/gateway/${path}`; + + return this.get(url, this.defaultConfig()); + } + + /** + * @param aiProviderId ai Provider identifier + * @param path raw provider API path after `/gateway/` + * @param request request body + * @see https://support.crowdin.com/developer/enterprise/api/v2/#tag/AI-Gateway/operation/api.ai.providers.gateway.enterprise.post + */ + organizationAiGatewayPost( + aiProviderId: number, + path: string, + request?: PlainObject, + ): Promise> { + const url = `${this.url}/ai/providers/${aiProviderId}/gateway/${path}`; + + return this.post(url, request, this.defaultConfig()); + } + + /** + * @param aiProviderId ai Provider identifier + * @param path raw provider API path after `/gateway/` + * @param request request body + * @see https://support.crowdin.com/developer/enterprise/api/v2/#tag/AI-Gateway/operation/api.ai.providers.gateway.enterprise.put + */ + organizationAiGatewayPut( + aiProviderId: number, + path: string, + request?: PlainObject, + ): Promise> { + const url = `${this.url}/ai/providers/${aiProviderId}/gateway/${path}`; + + return this.put(url, request, this.defaultConfig()); + } + + /** + * @param aiProviderId ai Provider identifier + * @param path raw provider API path after `/gateway/` + * @param request request body + * @see https://support.crowdin.com/developer/enterprise/api/v2/#tag/AI-Gateway/operation/api.ai.providers.gateway.enterprise.patch + */ + organizationAiGatewayPatch( + aiProviderId: number, + path: string, + request?: PlainObject, + ): Promise> { + const url = `${this.url}/ai/providers/${aiProviderId}/gateway/${path}`; + + return this.patch(url, request, this.defaultConfig()); + } + + /** + * @param aiProviderId ai Provider identifier + * @param path raw provider API path after `/gateway/` + * @see https://support.crowdin.com/developer/enterprise/api/v2/#tag/AI-Gateway/operation/api.ai.providers.gateway.enterprise.delete + */ + organizationAiGatewayDelete(aiProviderId: number, path: string): Promise> { + const url = `${this.url}/ai/providers/${aiProviderId}/gateway/${path}`; + + return this.delete(url, this.defaultConfig()); + } + // Community /** @@ -1112,6 +1182,84 @@ export class Ai extends CrowdinApi { return this.get(url, this.defaultConfig()); } + + /** + * @param userId user identifier + * @param aiProviderId ai Provider identifier + * @param path raw provider API path after `/gateway/` + * @see https://support.crowdin.com/developer/api/v2/#tag/AI-Gateway/operation/api.ai.providers.gateway.crowdin.get + */ + userAiGatewayGet(userId: number, aiProviderId: number, path: string): Promise> { + const url = `${this.url}/users/${userId}/ai/providers/${aiProviderId}/gateway/${path}`; + + return this.get(url, this.defaultConfig()); + } + + /** + * @param userId user identifier + * @param aiProviderId ai Provider identifier + * @param path raw provider API path after `/gateway/` + * @param request request body + * @see https://support.crowdin.com/developer/api/v2/#tag/AI-Gateway/operation/api.ai.providers.gateway.crowdin.post + */ + userAiGatewayPost( + userId: number, + aiProviderId: number, + path: string, + request?: PlainObject, + ): Promise> { + const url = `${this.url}/users/${userId}/ai/providers/${aiProviderId}/gateway/${path}`; + + return this.post(url, request, this.defaultConfig()); + } + + /** + * @param userId user identifier + * @param aiProviderId ai Provider identifier + * @param path raw provider API path after `/gateway/` + * @param request request body + * @see https://support.crowdin.com/developer/api/v2/#tag/AI-Gateway/operation/api.ai.providers.gateway.crowdin.put + */ + userAiGatewayPut( + userId: number, + aiProviderId: number, + path: string, + request?: PlainObject, + ): Promise> { + const url = `${this.url}/users/${userId}/ai/providers/${aiProviderId}/gateway/${path}`; + + return this.put(url, request, this.defaultConfig()); + } + + /** + * @param userId user identifier + * @param aiProviderId ai Provider identifier + * @param path raw provider API path after `/gateway/` + * @param request request body + * @see https://support.crowdin.com/developer/api/v2/#tag/AI-Gateway/operation/api.ai.providers.gateway.crowdin.patch + */ + userAiGatewayPatch( + userId: number, + aiProviderId: number, + path: string, + request?: PlainObject, + ): Promise> { + const url = `${this.url}/users/${userId}/ai/providers/${aiProviderId}/gateway/${path}`; + + return this.patch(url, request, this.defaultConfig()); + } + + /** + * @param userId user identifier + * @param aiProviderId ai Provider identifier + * @param path raw provider API path after `/gateway/` + * @see https://support.crowdin.com/developer/api/v2/#tag/AI-Gateway/operation/api.ai.providers.gateway.crowdin.delete + */ + userAiGatewayDelete(userId: number, aiProviderId: number, path: string): Promise> { + const url = `${this.url}/users/${userId}/ai/providers/${aiProviderId}/gateway/${path}`; + + return this.delete(url, this.defaultConfig()); + } } export namespace AiModel { diff --git a/tests/ai/api.test.ts b/tests/ai/api.test.ts index b49fcb39f..1eb859894 100644 --- a/tests/ai/api.test.ts +++ b/tests/ai/api.test.ts @@ -626,6 +626,46 @@ describe('AI API', () => { url: link, }, }) + .get(`/ai/providers/${aiProviderId}/gateway/chat/completions`, undefined, { + reqheaders: { + Authorization: `Bearer ${api.token}`, + }, + }) + .reply(200, { + data: field, + }) + .post(`/ai/providers/${aiProviderId}/gateway/chat/completions`, field, { + reqheaders: { + Authorization: `Bearer ${api.token}`, + }, + }) + .reply(200, { + data: field, + }) + .put(`/ai/providers/${aiProviderId}/gateway/chat/completions`, field, { + reqheaders: { + Authorization: `Bearer ${api.token}`, + }, + }) + .reply(200, { + data: field, + }) + .patch(`/ai/providers/${aiProviderId}/gateway/chat/completions`, field, { + reqheaders: { + Authorization: `Bearer ${api.token}`, + }, + }) + .reply(200, { + data: field, + }) + .delete(`/ai/providers/${aiProviderId}/gateway/chat/completions`, undefined, { + reqheaders: { + Authorization: `Bearer ${api.token}`, + }, + }) + .reply(200, { + data: field, + }) .get(`/users/${userId}/ai/settings/custom-placeholders`, undefined, { reqheaders: { Authorization: `Bearer ${api.token}`, @@ -1185,6 +1225,46 @@ describe('AI API', () => { data: { url: link, }, + }) + .get(`/users/${userId}/ai/providers/${aiProviderId}/gateway/chat/completions`, undefined, { + reqheaders: { + Authorization: `Bearer ${api.token}`, + }, + }) + .reply(200, { + data: field, + }) + .post(`/users/${userId}/ai/providers/${aiProviderId}/gateway/chat/completions`, field, { + reqheaders: { + Authorization: `Bearer ${api.token}`, + }, + }) + .reply(200, { + data: field, + }) + .put(`/users/${userId}/ai/providers/${aiProviderId}/gateway/chat/completions`, field, { + reqheaders: { + Authorization: `Bearer ${api.token}`, + }, + }) + .reply(200, { + data: field, + }) + .patch(`/users/${userId}/ai/providers/${aiProviderId}/gateway/chat/completions`, field, { + reqheaders: { + Authorization: `Bearer ${api.token}`, + }, + }) + .reply(200, { + data: field, + }) + .delete(`/users/${userId}/ai/providers/${aiProviderId}/gateway/chat/completions`, undefined, { + reqheaders: { + Authorization: `Bearer ${api.token}`, + }, + }) + .reply(200, { + data: field, }); }); @@ -1461,6 +1541,31 @@ describe('AI API', () => { expect(res.data.url).toBe(link); }); + it('Organization AI Gateway GET', async () => { + const res = await api.organizationAiGatewayGet(aiProviderId, 'chat/completions'); + expect(res.data).toStrictEqual(field); + }); + + it('Organization AI Gateway POST', async () => { + const res = await api.organizationAiGatewayPost(aiProviderId, 'chat/completions', field); + expect(res.data).toStrictEqual(field); + }); + + it('Organization AI Gateway PUT', async () => { + const res = await api.organizationAiGatewayPut(aiProviderId, 'chat/completions', field); + expect(res.data).toStrictEqual(field); + }); + + it('Organization AI Gateway PATCH', async () => { + const res = await api.organizationAiGatewayPatch(aiProviderId, 'chat/completions', field); + expect(res.data).toStrictEqual(field); + }); + + it('Organization AI Gateway DELETE', async () => { + const res = await api.organizationAiGatewayDelete(aiProviderId, 'chat/completions'); + expect(res.data).toStrictEqual(field); + }); + it('List AI User Custom Placeholders', async () => { const placeholders = await api.listAiUserCustomPlaceholders(userId); expect(placeholders.data.length).toBe(1); @@ -1729,4 +1834,29 @@ describe('AI API', () => { const res = await api.downloadAiUserFileTranslationStrings(userId, jobId); expect(res.data.url).toBe(link); }); + + it('User AI Gateway GET', async () => { + const res = await api.userAiGatewayGet(userId, aiProviderId, 'chat/completions'); + expect(res.data).toStrictEqual(field); + }); + + it('User AI Gateway POST', async () => { + const res = await api.userAiGatewayPost(userId, aiProviderId, 'chat/completions', field); + expect(res.data).toStrictEqual(field); + }); + + it('User AI Gateway PUT', async () => { + const res = await api.userAiGatewayPut(userId, aiProviderId, 'chat/completions', field); + expect(res.data).toStrictEqual(field); + }); + + it('User AI Gateway PATCH', async () => { + const res = await api.userAiGatewayPatch(userId, aiProviderId, 'chat/completions', field); + expect(res.data).toStrictEqual(field); + }); + + it('User AI Gateway DELETE', async () => { + const res = await api.userAiGatewayDelete(userId, aiProviderId, 'chat/completions'); + expect(res.data).toStrictEqual(field); + }); });