From 80aed3e6e36a9adc9bb12470a2fb28e97bc29fe8 Mon Sep 17 00:00:00 2001 From: okwuchi Date: Mon, 9 Mar 2026 15:27:10 +0000 Subject: [PATCH 1/5] feat(ai): add support for AI Translate Strings method --- src/Crowdin.Api/AI/AiApiExecutor.cs | 16 +++++ .../AI/AiTranslateStringsRequest.cs | 40 +++++++++++ .../AI/AiTranslateStringsResponse.cs | 19 ++++++ src/Crowdin.Api/AI/IAiApiExecutor.cs | 3 + .../Crowdin.Api.UnitTesting.csproj | 9 +++ .../Resources/AI_TranslateStrings.Designer.cs | 60 +++++++++++++++++ .../Resources/AI_TranslateStrings.resx | 55 ++++++++++++++++ .../Tests/AI/AiTranslateStringsApiTests.cs | 66 +++++++++++++++++++ 8 files changed, 268 insertions(+) create mode 100644 src/Crowdin.Api/AI/AiTranslateStringsRequest.cs create mode 100644 src/Crowdin.Api/AI/AiTranslateStringsResponse.cs create mode 100644 tests/Crowdin.Api.UnitTesting/Resources/AI_TranslateStrings.Designer.cs create mode 100644 tests/Crowdin.Api.UnitTesting/Resources/AI_TranslateStrings.resx create mode 100644 tests/Crowdin.Api.UnitTesting/Tests/AI/AiTranslateStringsApiTests.cs diff --git a/src/Crowdin.Api/AI/AiApiExecutor.cs b/src/Crowdin.Api/AI/AiApiExecutor.cs index 304bd87d..01dac608 100644 --- a/src/Crowdin.Api/AI/AiApiExecutor.cs +++ b/src/Crowdin.Api/AI/AiApiExecutor.cs @@ -515,6 +515,22 @@ public async Task> ListSupportedAiProvide return _jsonParser.ParseResponseList(result.JsonObject); } + /// + /// AI Translate Strings. Documentation: + /// Crowdin File Based API + /// Crowdin String Based API + /// Crowdin Enterprise API + /// + [PublicAPI] + public async Task TranslateStrings( + long? userId, + AiTranslateStringsRequest request) + { + string url = AddUserIdIfAvailable(userId, "/ai/translate"); + CrowdinApiResult result = await _apiClient.SendPostRequest(url, request); + return _jsonParser.ParseResponseObject(result.JsonObject); + } + #region Helper methods private static string AddUserIdIfAvailable(long? userId, string baseUrl) diff --git a/src/Crowdin.Api/AI/AiTranslateStringsRequest.cs b/src/Crowdin.Api/AI/AiTranslateStringsRequest.cs new file mode 100644 index 00000000..4c61d8c0 --- /dev/null +++ b/src/Crowdin.Api/AI/AiTranslateStringsRequest.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using JetBrains.Annotations; +using Newtonsoft.Json; + +namespace Crowdin.Api.AI +{ + [PublicAPI] + public class AiTranslateStringsRequest + { + [JsonProperty("strings")] + public ICollection Strings { get; set; } + + [JsonProperty("targetLanguageId")] + public string TargetLanguageId { get; set; } + + [JsonProperty("sourceLanguageId")] + public string? SourceLanguageId { get; set; } + + [JsonProperty("tmIds")] + public ICollection? TmIds { get; set; } + + [JsonProperty("glossaryIds")] + public ICollection? GlossaryIds { get; set; } + + [JsonProperty("aiPromptId")] + public int? AiPromptId { get; set; } + + [JsonProperty("aiProviderId")] + public int? AiProviderId { get; set; } + + [JsonProperty("aiModelId")] + public string? AiModelId { get; set; } + + [JsonProperty("instructions")] + public ICollection? Instructions { get; set; } + + [JsonProperty("attachmentIds")] + public ICollection? AttachmentIds { get; set; } + } +} \ No newline at end of file diff --git a/src/Crowdin.Api/AI/AiTranslateStringsResponse.cs b/src/Crowdin.Api/AI/AiTranslateStringsResponse.cs new file mode 100644 index 00000000..4c56b9d4 --- /dev/null +++ b/src/Crowdin.Api/AI/AiTranslateStringsResponse.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using JetBrains.Annotations; +using Newtonsoft.Json; + +namespace Crowdin.Api.AI +{ + [PublicAPI] + public class AiTranslateStringsResponse + { + [JsonProperty("sourceLanguageId")] + public string SourceLanguageId { get; set; } = null!; + + [JsonProperty("targetLanguageId")] + public string TargetLanguageId { get; set; } = null!; + + [JsonProperty("translations")] + public ICollection Translations { get; set; } = null!; + } +} \ No newline at end of file diff --git a/src/Crowdin.Api/AI/IAiApiExecutor.cs b/src/Crowdin.Api/AI/IAiApiExecutor.cs index 7e128656..054d88d5 100644 --- a/src/Crowdin.Api/AI/IAiApiExecutor.cs +++ b/src/Crowdin.Api/AI/IAiApiExecutor.cs @@ -120,5 +120,8 @@ Task> ListSupportedAiProviderModels( string? providerType = null, bool? enabled = null, string? orderBy = null); + Task TranslateStrings( + long? userId, + AiTranslateStringsRequest request); } } \ No newline at end of file diff --git a/tests/Crowdin.Api.UnitTesting/Crowdin.Api.UnitTesting.csproj b/tests/Crowdin.Api.UnitTesting/Crowdin.Api.UnitTesting.csproj index 986e4463..7008fe3d 100644 --- a/tests/Crowdin.Api.UnitTesting/Crowdin.Api.UnitTesting.csproj +++ b/tests/Crowdin.Api.UnitTesting/Crowdin.Api.UnitTesting.csproj @@ -265,6 +265,10 @@ ResXFileCodeGenerator Tasks_Comments.Designer.cs + + ResXFileCodeGenerator + AI_TranslateStrings.Designer.cs + @@ -563,6 +567,11 @@ True Tasks_Comments.resx + + True + True + AI_TranslateStrings.resx + diff --git a/tests/Crowdin.Api.UnitTesting/Resources/AI_TranslateStrings.Designer.cs b/tests/Crowdin.Api.UnitTesting/Resources/AI_TranslateStrings.Designer.cs new file mode 100644 index 00000000..6f452b93 --- /dev/null +++ b/tests/Crowdin.Api.UnitTesting/Resources/AI_TranslateStrings.Designer.cs @@ -0,0 +1,60 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Crowdin.Api.UnitTesting.Resources { + using System; + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [System.Diagnostics.DebuggerNonUserCodeAttribute()] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class AI_TranslateStrings { + + private static System.Resources.ResourceManager resourceMan; + + private static System.Globalization.CultureInfo resourceCulture; + + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal AI_TranslateStrings() { + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Resources.ResourceManager ResourceManager { + get { + if (object.Equals(null, resourceMan)) { + System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Crowdin.Api.UnitTesting.Resources.AI_TranslateStrings", typeof(AI_TranslateStrings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + internal static string TranslateStrings_Request { + get { + return ResourceManager.GetString("TranslateStrings_Request", resourceCulture); + } + } + + internal static string CommonResponses_TranslateStrings { + get { + return ResourceManager.GetString("CommonResponses_TranslateStrings", resourceCulture); + } + } + } +} diff --git a/tests/Crowdin.Api.UnitTesting/Resources/AI_TranslateStrings.resx b/tests/Crowdin.Api.UnitTesting/Resources/AI_TranslateStrings.resx new file mode 100644 index 00000000..58f94f47 --- /dev/null +++ b/tests/Crowdin.Api.UnitTesting/Resources/AI_TranslateStrings.resx @@ -0,0 +1,55 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + { + "strings": [ + "Some text to translate!" + ], + "targetLanguageId": "uk", + "sourceLanguageId": "en", + "tmIds": [ + 123 + ], + "glossaryIds": [ + 456 + ], + "aiPromptId": 789, + "instructions": [ + "Keep a formal tone" + ], + "attachmentIds": [ + 123 + ] +} + + + { + "data": { + "sourceLanguageId": "en", + "targetLanguageId": "uk", + "translations": [ + "Перекладений текст 1", + "Перекладений текст 2" + ] + } +} + + \ No newline at end of file diff --git a/tests/Crowdin.Api.UnitTesting/Tests/AI/AiTranslateStringsApiTests.cs b/tests/Crowdin.Api.UnitTesting/Tests/AI/AiTranslateStringsApiTests.cs new file mode 100644 index 00000000..2f02d586 --- /dev/null +++ b/tests/Crowdin.Api.UnitTesting/Tests/AI/AiTranslateStringsApiTests.cs @@ -0,0 +1,66 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using Crowdin.Api.AI; +using Crowdin.Api.Core; +using Crowdin.Api.UnitTesting.Resources; +using Moq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Crowdin.Api.UnitTesting.Tests.AI; + +public class AiTranslateStringsApiTests +{ + private static readonly JsonSerializerSettings JsonSettings = TestUtils.CreateJsonSerializerOptions(); + + [Fact] + public async Task TranslateStrings() + { + const int userId = 1; + + var request = new AiTranslateStringsRequest + { + Strings = new[] { "Some text to translate!" }, + TargetLanguageId = "uk", + SourceLanguageId = "en", + TmIds = new[] { 123 }, + GlossaryIds = new[] { 456 }, + AiPromptId = 789, + Instructions = new[] { "Keep a formal tone" }, + AttachmentIds = new[] { 123 } + }; + + string actualRequestJson = JsonConvert.SerializeObject(request, JsonSettings); + string expectedRequestJson = TestUtils.CompactJson(AI_TranslateStrings.TranslateStrings_Request); + Assert.Equal(expectedRequestJson, actualRequestJson); + + Mock mockClient = TestUtils.CreateMockClientWithDefaultParser(); + + var url = $"/users/{userId}/ai/translate"; + + mockClient + .Setup(client => client.SendPostRequest(url, request, null)) + .ReturnsAsync(new CrowdinApiResult + { + StatusCode = HttpStatusCode.OK, + JsonObject = JObject.Parse(AI_TranslateStrings.CommonResponses_TranslateStrings) + }); + + var executor = new AiApiExecutor(mockClient.Object); + AiTranslateStringsResponse response = await executor.TranslateStrings(userId, request); + + Assert_TranslateStrings(response); + } + + private static void Assert_TranslateStrings(AiTranslateStringsResponse? response) + { + ArgumentNullException.ThrowIfNull(response); + + Assert.Equal("en", response.SourceLanguageId); + Assert.Equal("uk", response.TargetLanguageId); + Assert.NotNull(response.Translations); + Assert.Equal(2, response.Translations.Count); + } +} \ No newline at end of file From 570c05e720c3734f85a6d1029d2208792da8d83e Mon Sep 17 00:00:00 2001 From: Okwuchi Nneka Uzoigwe Date: Fri, 13 Mar 2026 14:18:12 +0000 Subject: [PATCH 2/5] Update src/Crowdin.Api/AI/AiTranslateStringsRequest.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Crowdin.Api/AI/AiTranslateStringsRequest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Crowdin.Api/AI/AiTranslateStringsRequest.cs b/src/Crowdin.Api/AI/AiTranslateStringsRequest.cs index 4c61d8c0..c70c3de7 100644 --- a/src/Crowdin.Api/AI/AiTranslateStringsRequest.cs +++ b/src/Crowdin.Api/AI/AiTranslateStringsRequest.cs @@ -17,10 +17,10 @@ public class AiTranslateStringsRequest public string? SourceLanguageId { get; set; } [JsonProperty("tmIds")] - public ICollection? TmIds { get; set; } + public ICollection? TmIds { get; set; } [JsonProperty("glossaryIds")] - public ICollection? GlossaryIds { get; set; } + public ICollection? GlossaryIds { get; set; } [JsonProperty("aiPromptId")] public int? AiPromptId { get; set; } From 488fc8fcaf9605389e9b03925e2f32a570f5dfa2 Mon Sep 17 00:00:00 2001 From: Okwuchi Nneka Uzoigwe Date: Fri, 13 Mar 2026 14:18:21 +0000 Subject: [PATCH 3/5] Update src/Crowdin.Api/AI/AiTranslateStringsRequest.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Crowdin.Api/AI/AiTranslateStringsRequest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Crowdin.Api/AI/AiTranslateStringsRequest.cs b/src/Crowdin.Api/AI/AiTranslateStringsRequest.cs index c70c3de7..f9c344e5 100644 --- a/src/Crowdin.Api/AI/AiTranslateStringsRequest.cs +++ b/src/Crowdin.Api/AI/AiTranslateStringsRequest.cs @@ -35,6 +35,6 @@ public class AiTranslateStringsRequest public ICollection? Instructions { get; set; } [JsonProperty("attachmentIds")] - public ICollection? AttachmentIds { get; set; } + public ICollection? AttachmentIds { get; set; } } } \ No newline at end of file From ef1fb1bdd0c6d6004973ec4993b0a1c8464b24cf Mon Sep 17 00:00:00 2001 From: Okwuchi Nneka Uzoigwe Date: Fri, 13 Mar 2026 14:18:37 +0000 Subject: [PATCH 4/5] Update src/Crowdin.Api/AI/AiTranslateStringsRequest.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Crowdin.Api/AI/AiTranslateStringsRequest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Crowdin.Api/AI/AiTranslateStringsRequest.cs b/src/Crowdin.Api/AI/AiTranslateStringsRequest.cs index f9c344e5..58a7ff6f 100644 --- a/src/Crowdin.Api/AI/AiTranslateStringsRequest.cs +++ b/src/Crowdin.Api/AI/AiTranslateStringsRequest.cs @@ -23,10 +23,10 @@ public class AiTranslateStringsRequest public ICollection? GlossaryIds { get; set; } [JsonProperty("aiPromptId")] - public int? AiPromptId { get; set; } + public long? AiPromptId { get; set; } [JsonProperty("aiProviderId")] - public int? AiProviderId { get; set; } + public long? AiProviderId { get; set; } [JsonProperty("aiModelId")] public string? AiModelId { get; set; } From f5e88a2cec238e47569f57fb9be6488ca089880d Mon Sep 17 00:00:00 2001 From: okwuchi Date: Mon, 16 Mar 2026 09:34:14 +0000 Subject: [PATCH 5/5] fix build error --- .../Tests/AI/AiTranslateStringsApiTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Crowdin.Api.UnitTesting/Tests/AI/AiTranslateStringsApiTests.cs b/tests/Crowdin.Api.UnitTesting/Tests/AI/AiTranslateStringsApiTests.cs index 2f02d586..fe85e779 100644 --- a/tests/Crowdin.Api.UnitTesting/Tests/AI/AiTranslateStringsApiTests.cs +++ b/tests/Crowdin.Api.UnitTesting/Tests/AI/AiTranslateStringsApiTests.cs @@ -25,13 +25,13 @@ public async Task TranslateStrings() Strings = new[] { "Some text to translate!" }, TargetLanguageId = "uk", SourceLanguageId = "en", - TmIds = new[] { 123 }, - GlossaryIds = new[] { 456 }, + TmIds = new[] { 123L }, + GlossaryIds = new[] { 456L }, AiPromptId = 789, Instructions = new[] { "Keep a formal tone" }, - AttachmentIds = new[] { 123 } + AttachmentIds = new[] { 123L } }; - + string actualRequestJson = JsonConvert.SerializeObject(request, JsonSettings); string expectedRequestJson = TestUtils.CompactJson(AI_TranslateStrings.TranslateStrings_Request); Assert.Equal(expectedRequestJson, actualRequestJson);