Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions src/Crowdin.Api/AI/AiApiExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,79 @@ public async Task<AiSettings> EditAiSettings(long? userId, IEnumerable<AiSetting
return _jsonParser.ParseResponseObject<AiSettings>(result.JsonObject);
}

#endregion

#region File Translations

/// <summary>
/// AI File Translations. Documentation:
/// <a href="https://support.crowdin.com/developer/api/v2/#tag/AI/operation/api.users.ai.file-translations.post">Crowdin File Based API</a>
/// <a href="https://support.crowdin.com/developer/api/v2/string-based/#tag/AI/operation/api.users.ai.file-translations.post">Crowdin String Based API</a>
/// <a href="https://support.crowdin.com/developer/enterprise/api/v2/#tag/AI/operation/api.ai.file-translations.post">Crowdin Enterprise API</a>
/// </summary>
[PublicAPI]
public async Task<AiFileTranslationsStatus> AiFileTranslations(long? userId, AiFileTranslationsRequest request)
{
string url = AddUserIdIfAvailable(userId, "/ai/file-translations");
CrowdinApiResult result = await _apiClient.SendPostRequest(url, request);
return _jsonParser.ParseResponseObject<AiFileTranslationsStatus>(result.JsonObject);
}

Comment on lines +484 to +490
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implementation method name AiFileTranslations(...) is noun-like and inconsistent with the executor's other verb-based method names. If you rename the interface method for consistency/clarity, update this implementation accordingly to keep the public API self-explanatory.

Suggested change
public async Task<AiFileTranslationsStatus> AiFileTranslations(long? userId, AiFileTranslationsRequest request)
{
string url = AddUserIdIfAvailable(userId, "/ai/file-translations");
CrowdinApiResult result = await _apiClient.SendPostRequest(url, request);
return _jsonParser.ParseResponseObject<AiFileTranslationsStatus>(result.JsonObject);
}
public async Task<AiFileTranslationsStatus> StartFileTranslations(long? userId, AiFileTranslationsRequest request)
{
string url = AddUserIdIfAvailable(userId, "/ai/file-translations");
CrowdinApiResult result = await _apiClient.SendPostRequest(url, request);
return _jsonParser.ParseResponseObject<AiFileTranslationsStatus>(result.JsonObject);
}
Task<AiFileTranslationsStatus> IAiApiExecutor.AiFileTranslations(long? userId, AiFileTranslationsRequest request)
{
return StartFileTranslations(userId, request);
}

Copilot uses AI. Check for mistakes.
/// <summary>
/// Get File Translations Status. Documentation:
/// <a href="https://support.crowdin.com/developer/api/v2/#tag/AI/operation/api.users.ai.file-translations.get">Crowdin File Based API</a>
/// <a href="https://support.crowdin.com/developer/api/v2/string-based/#tag/AI/operation/api.users.ai.file-translations.get">Crowdin String Based API</a>
/// <a href="https://support.crowdin.com/developer/enterprise/api/v2/#tag/AI/operation/api.ai.file-translations.get">Crowdin Enterprise API</a>
/// </summary>
[PublicAPI]
public async Task<AiFileTranslationsStatus> GetFileTranslationsStatus(long? userId, string jobIdentifier)
{
string url = AddUserIdIfAvailable(userId, $"/ai/file-translations/{jobIdentifier}");
CrowdinApiResult result = await _apiClient.SendGetRequest(url);
return _jsonParser.ParseResponseObject<AiFileTranslationsStatus>(result.JsonObject);
}

/// <summary>
/// Cancel File Translations. Documentation:
/// <a href="https://support.crowdin.com/developer/api/v2/#tag/AI/operation/api.users.ai.file-translations.delete">Crowdin File Based API</a>
/// <a href="https://support.crowdin.com/developer/api/v2/string-based/#tag/AI/operation/api.users.ai.file-translations.delete">Crowdin String Based API</a>
/// <a href="https://support.crowdin.com/developer/enterprise/api/v2/#tag/AI/operation/api.ai.file-translations.delete">Crowdin Enterprise API</a>
/// </summary>
[PublicAPI]
public async Task CancelFileTranslations(long? userId, string jobIdentifier)
{
string url = AddUserIdIfAvailable(userId, $"/ai/file-translations/{jobIdentifier}");
HttpStatusCode statusCode = await _apiClient.SendDeleteRequest(url);
Utils.ThrowIfStatusNot204(statusCode, $"File Translation {jobIdentifier} removal failed");
}

/// <summary>
/// Download Translated File. Documentation:
/// <a href="https://support.crowdin.com/developer/api/v2/#tag/AI/operation/api.users.ai.file-translations.download">Crowdin File Based API</a>
/// <a href="https://support.crowdin.com/developer/api/v2/string-based/#tag/AI/operation/api.users.ai.file-translations.download">Crowdin String Based API</a>
/// <a href="https://support.crowdin.com/developer/enterprise/api/v2/#tag/AI/operation/api.ai.file-translations.download">Crowdin Enterprise API</a>
/// </summary>
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DownloadTranslatedFile is missing the [PublicAPI] attribute, while other public methods in this executor are consistently annotated. For consistency (and to match the other AI endpoints), add [PublicAPI] to this method as well.

Suggested change
/// </summary>
/// </summary>
[PublicAPI]

Copilot uses AI. Check for mistakes.
public async Task<DownloadLink> DownloadTranslatedFile(long? userId, string jobIdentifier)
{
string url = AddUserIdIfAvailable(userId, $"/ai/file-translations/{jobIdentifier}/download");
CrowdinApiResult result = await _apiClient.SendGetRequest(url);
return _jsonParser.ParseResponseObject<DownloadLink>(result.JsonObject);
}

/// <summary>
/// Download Translated File Strings. Documentation:
/// <a href="https://support.crowdin.com/developer/api/v2/#tag/AI/operation/api.users.ai.file-translations.download-strings">Crowdin File Based API</a>
/// <a href="https://support.crowdin.com/developer/api/v2/string-based/#tag/AI/operation/api.users.ai.file-translations.download-strings">Crowdin String Based API</a>
/// <a href="https://support.crowdin.com/developer/enterprise/api/v2/#tag/AI/operation/api.ai.file-translations.download-strings">Crowdin Enterprise API</a>
/// </summary>
[PublicAPI]
public async Task<DownloadLink> DownloadTranslatedFileStrings(long? userId, string jobIdentifier)
{
string url = AddUserIdIfAvailable(userId, $"/ai/file-translations/{jobIdentifier}/translations");
CrowdinApiResult result = await _apiClient.SendGetRequest(url);
return _jsonParser.ParseResponseObject<DownloadLink>(result.JsonObject);
}

#endregion

/// <summary>
Expand Down
50 changes: 50 additions & 0 deletions src/Crowdin.Api/AI/AiFileTranslationsRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@

using System.Collections.Generic;
using Crowdin.Api.SourceFiles;
using JetBrains.Annotations;
using Newtonsoft.Json;

#nullable enable

namespace Crowdin.Api.AI
{
[PublicAPI]
public class AiFileTranslationsRequest
{
[JsonProperty("storageId")]
public long StorageId { get; set; }

[JsonProperty("sourceLanguageId")]
public string? SourceLanguageId { get; set; }

[JsonProperty("targetLanguageId")]
public string TargetLanguageId { get; set; } = null!;

[JsonProperty("type")]
public ProjectFileType? Type { get; set; }

[JsonProperty("parserVersion")]
public int? ParserVersion { get; set; }

[JsonProperty("tmIds")]
public ICollection<long>? TmIds { get; set; }

[JsonProperty("glossaryIds")]
public ICollection<long>? GlossaryIds { get; set; }

[JsonProperty("aiPromptId")]
public long? AiPromptId { get; set; }

[JsonProperty("aiProviderId")]
public long? AiProviderId { get; set; }

[JsonProperty("aiModelId")]
public string? AiModelId { get; set; }

[JsonProperty("instructions")]
public ICollection<string>? Instructions { get; set; }

[JsonProperty("attachmentIds")]
public ICollection<long>? AttachmentIds { get; set; }
}
}
25 changes: 25 additions & 0 deletions src/Crowdin.Api/AI/AiFileTranslationsStage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

using System.ComponentModel;
using JetBrains.Annotations;

namespace Crowdin.Api.AI
{
[PublicAPI]
public enum AiFileTranslationsStage
{
[Description("start")]
Start,

[Description("import")]
Import,

[Description("translate")]
Translate,

[Description("export")]
Export,

[Description("done")]
Done
}
}
76 changes: 76 additions & 0 deletions src/Crowdin.Api/AI/AiFileTranslationsStatus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@

using System;
using Crowdin.Api.SourceFiles;
using JetBrains.Annotations;
using Newtonsoft.Json;

#nullable enable

namespace Crowdin.Api.AI
{
[PublicAPI]
public class AiFileTranslationsStatus
{
[JsonProperty("identifier")]
public string Identifier { get; set; } = null!;

[JsonProperty("status")]
public OperationStatus Status { get; set; }

[JsonProperty("progress")]
public int Progress { get; set; }

[JsonProperty("attributes")]
public AttributesObject Attributes { get; set; } = null!;

[PublicAPI]
public class AttributesObject
{
[JsonProperty("stage")]
public AiFileTranslationsStage Stage { get; set; }

[JsonProperty("error")]
public ErrorObject? Error { get; set; }

[JsonProperty("downloadName")]
public string? DownloadName { get; set; }

[JsonProperty("sourceLanguageId")]
public string? SourceLanguageId { get; set; }

[JsonProperty("targetLanguageId")]
public string? TargetLanguageId { get; set; }

[JsonProperty("originalFileName")]
public string? OriginalFileName { get; set; }

[JsonProperty("detectedType")]
public ProjectFileType? DetectedType { get; set; }

[JsonProperty("parserVersion")]
public int? ParserVersion { get; set; }

[PublicAPI]
public class ErrorObject
{
[JsonProperty("stage")]
public AiFileTranslationsStage Stage { get; set; }

[JsonProperty("message")]
public string Message { get; set; } = null!;
}
}

[JsonProperty("createdAt")]
public DateTimeOffset CreatedAt { get; set; }

[JsonProperty("updatedAt")]
public DateTimeOffset? UpdatedAt { get; set; }

[JsonProperty("startedAt")]
public DateTimeOffset? StartedAt { get; set; }

[JsonProperty("finishedAt")]
public DateTimeOffset? FinishedAt { get; set; }
}
}
44 changes: 44 additions & 0 deletions src/Crowdin.Api/AI/IAiApiExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,50 @@ Task<AiPromptResource> EditAiPrompt(

Task<AiSettings> EditAiSettings(long? userId, IEnumerable<AiSettingsPatch> patches);

#endregion

#region File Translations

/// <summary>
/// AI File Translations. Documentation:
/// <a href="https://support.crowdin.com/developer/api/v2/#tag/AI/operation/api.users.ai.file-translations.post">Crowdin File Based API</a>
/// <a href="https://support.crowdin.com/developer/api/v2/string-based/#tag/AI/operation/api.users.ai.file-translations.post">Crowdin String Based API</a>
/// <a href="https://support.crowdin.com/developer/enterprise/api/v2/#tag/AI/operation/api.ai.file-translations.post">Crowdin Enterprise API</a>
/// </summary>
Task<AiFileTranslationsStatus> AiFileTranslations(long? userId, AiFileTranslationsRequest request);

Comment on lines +113 to +120
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method name AiFileTranslations(...) reads like a noun/resource name rather than an action, which is inconsistent with the rest of IAiApiExecutor (e.g., GenerateAiReport, TranslateStrings, DownloadAiReport). Consider renaming this to a verb-based name (e.g., StartFileTranslation / TranslateFile) so the public API is clearer and more consistent.

Copilot uses AI. Check for mistakes.
/// <summary>
/// Get File Translations Status. Documentation:
/// <a href="https://support.crowdin.com/developer/api/v2/#tag/AI/operation/api.users.ai.file-translations.get">Crowdin File Based API</a>
/// <a href="https://support.crowdin.com/developer/api/v2/string-based/#tag/AI/operation/api.users.ai.file-translations.get">Crowdin String Based API</a>
/// <a href="https://support.crowdin.com/developer/enterprise/api/v2/#tag/AI/operation/api.ai.file-translations.get">Crowdin Enterprise API</a>
/// </summary>
Task<AiFileTranslationsStatus> GetFileTranslationsStatus(long? userId, string jobIdentifier);

/// <summary>
/// Cancel File Translations. Documentation:
/// <a href="https://support.crowdin.com/developer/api/v2/#tag/AI/operation/api.users.ai.file-translations.delete">Crowdin File Based API</a>
/// <a href="https://support.crowdin.com/developer/api/v2/string-based/#tag/AI/operation/api.users.ai.file-translations.delete">Crowdin String Based API</a>
/// <a href="https://support.crowdin.com/developer/enterprise/api/v2/#tag/AI/operation/api.ai.file-translations.delete">Crowdin Enterprise API</a>
/// </summary>
Task CancelFileTranslations(long? userId, string jobIdentifier);

/// <summary>
/// Download Translated File. Documentation:
/// <a href="https://support.crowdin.com/developer/api/v2/#tag/AI/operation/api.users.ai.file-translations.download">Crowdin File Based API</a>
/// <a href="https://support.crowdin.com/developer/api/v2/string-based/#tag/AI/operation/api.users.ai.file-translations.download">Crowdin String Based API</a>
/// <a href="https://support.crowdin.com/developer/enterprise/api/v2/#tag/AI/operation/api.ai.file-translations.download">Crowdin Enterprise API</a>
/// </summary>
Task<DownloadLink> DownloadTranslatedFile(long? userId, string jobIdentifier);

/// <summary>
/// Download Translated File Strings. Documentation:
/// <a href="https://support.crowdin.com/developer/api/v2/#tag/AI/operation/api.users.ai.file-translations.download-strings">Crowdin File Based API</a>
/// <a href="https://support.crowdin.com/developer/api/v2/string-based/#tag/AI/operation/api.users.ai.file-translations.download-strings">Crowdin String Based API</a>
/// <a href="https://support.crowdin.com/developer/enterprise/api/v2/#tag/AI/operation/api.ai.file-translations.download-strings">Crowdin Enterprise API</a>
/// </summary>
Task<DownloadLink> DownloadTranslatedFileStrings(long? userId, string jobIdentifier);

#endregion

Task<AiProxyChatCompletion> CreateAiProxyChatCompletion(
Expand Down
17 changes: 17 additions & 0 deletions tests/Crowdin.Api.UnitTesting/CommonResponsesAssertUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@

using System;
using Xunit;

namespace Crowdin.Api.UnitTesting
{
public static class CommonResponsesAssertUtils
{
public static void Assert_DownloadLink(DownloadLink? expectedDownloadLink)
{
ArgumentNullException.ThrowIfNull(expectedDownloadLink);

Assert.Equal("https://test.com", expectedDownloadLink.Url);
Assert.Equal(DateTimeOffset.Parse("2019-09-20T10:31:21+00:00"), expectedDownloadLink.ExpireIn);
Comment on lines +9 to +14
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assert_DownloadLink parameter name expectedDownloadLink is misleading because the method asserts against the actual response value passed in by callers. Rename it to something like downloadLink/actualDownloadLink to make intent clearer.

Suggested change
public static void Assert_DownloadLink(DownloadLink? expectedDownloadLink)
{
ArgumentNullException.ThrowIfNull(expectedDownloadLink);
Assert.Equal("https://test.com", expectedDownloadLink.Url);
Assert.Equal(DateTimeOffset.Parse("2019-09-20T10:31:21+00:00"), expectedDownloadLink.ExpireIn);
public static void Assert_DownloadLink(DownloadLink? downloadLink)
{
ArgumentNullException.ThrowIfNull(downloadLink);
Assert.Equal("https://test.com", downloadLink.Url);
Assert.Equal(DateTimeOffset.Parse("2019-09-20T10:31:21+00:00"), downloadLink.ExpireIn);

Copilot uses AI. Check for mistakes.
}
}
}
18 changes: 18 additions & 0 deletions tests/Crowdin.Api.UnitTesting/Crowdin.Api.UnitTesting.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,14 @@
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>AI_TranslateStrings.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="Resources\AI_FileTranslations.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>AI_FileTranslations.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Common.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Common.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>

<ItemGroup>
Expand Down Expand Up @@ -572,6 +580,16 @@
<AutoGen>True</AutoGen>
<DependentUpon>AI_TranslateStrings.resx</DependentUpon>
</Compile>
<Compile Update="Resources\AI_FileTranslations.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>AI_FileTranslations.resx</DependentUpon>
</Compile>
<Compile Update="Resources\Common.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Common.resx</DependentUpon>
</Compile>
</ItemGroup>

<PropertyGroup>
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading