Giter VIP home page Giter VIP logo

crowdin-api-client-dotnet's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

crowdin-api-client-dotnet's Issues

TranslationsApiExecutor.ListProjectBuilds doesn't seem to handle the response from the server.

When calling TranslationsApiExecutor.ListProjectBuilds, I'm getting the following error:

Newtonsoft.Json.JsonSerializationException: 'Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'Crowdin.Api.Translations.TranslationProjectBuild+AttributesHolder[]' because the type requires a JSON array (e.g. [1,2,3]) to deserialize correctly.

When I look at the response from the server, it appears that Attributes are not an array, but just an single object containing the attributes (this would make sense since it's the TranslationProjectBuild that is a list of built translations). The API reference also looks incorrect compared to what I get back from the server.

What I'm worried about is that the server might respond with an array of attributes in some cases - which would make it hard to work as a C# deserialized object. I do have a pull request ready to go for this one if I can get confirmation it never returns an array.

For reference, this is the response I got from the server:

{
    "data": [
        {
            "data": {
                "id": 13,
                "projectId": 12345,
                "status": "finished",
                "progress": 100,
                "createdAt": "2022-03-31T18:31:28+00:00",
                "updatedAt": "2022-03-31T18:45:15+00:00",
                "finishedAt": "2022-03-31T18:45:15+00:00",
                "attributes": {
                    "branchId": null,
                    "directoryId": null,
                    "targetLanguageIds": [],
                    "skipUntranslatedStrings": false,
                    "skipUntranslatedFiles": false,
                    "exportApprovedOnly": false
                }
            }
        }
    ],
    "pagination": {
        "offset": 0,
        "limit": 25
    }
}

Invalid MediaType when calling EditTaskArchivedStatus

Hello,
calling EditTaskArchivedStatus raises an exception about mediatype.

Patch in CrowdinApiClient.cs

        private HttpContent CreateJsonContent(object body, bool isPatch = false)
        {
            string bodyJson = JsonConvert.SerializeObject(body, DefaultJsonSerializerOptions);

            MediaTypeHeaderValue contentType = isPatch
//                ? MediaTypeHeaderValue.Parse("application/json-patch+json")
                ? MediaTypeHeaderValue.Parse("application/json")
                : DefaultContentType;
            
            return new StringContent(bodyJson, Encoding.UTF8, contentType.MediaType);
        }

File ImportOptions improvement

We've added new possible import options for files:

  • Add File: new ImportOptions possible value - Docx File Import Options (including the response)

  • Update or Restore File: new ImportOptions possible value - Docx File Import Options (including the response)

  • Edit File: new /importOptions/* path parameters

So these changes should be reflected in this API Client.

C# API Client: AddTag doesn't work

Is this a regression?
Yes

In which version?
2.12 --> workaround possible
2.14 --> not more workaround

Description
We’re currently working on the C# Crowdin API’s AddTag 2.14. version. Using the clientWrapper we faced this issue:

Unexpected character encountered while parsing value: {. Path 'errors[0].message', line 6, position 18.

I downgraded the version to 2.12, same error happen. On this version, sendPostRequest is accessible so was easier to debug it:

 string subUrl = FormUrl_ScreenshotTags(projectId, screenshotId);
  
CrowdinApiResult crowdinApiResult = await _client.SendPostRequest(subUrl, requests);
           
 _jsonParser.ParseResponseObject<Tag[]>(crowdinApiResult.JsonObject);

This code correspond to the AddTag inside ScreenshotsApiExecutor.cs

crowdinApiResult is well responding with a property JsonObject that contain correct information. But it fails on _jsonParser.ParseResponseObject, since JsonObject data is an array and ParseResponseObject expects an object.

Here is the exact error of it:

Cannot deserialize the current JSON array (e.g. [1,2,3]) into type 'Crowdin.Api.Screenshots.Tag' because the type requires a JSON object (e.g. {"name":"value"}) to deserialize correctly.
To fix this error either change the JSON to a JSON object (e.g. {"name":"value"}) or change the deserialized type to an array or a type that implements a collection interface (e.g. ICollection, IList) like List that can be deserialized from a JSON array. JsonArrayAttribute can also be added to the type to force it to deserialize from a JSON array.
Path '', line 1, position 1.

That issue could potentially be not only on AddTag, but everywhere where JsonObject is used and we expect an array json as response.

Worth to mention that: there is no test case for AddTag but only for ReplaceTag if i see well the Screenshot -> TagsApiTest.cs

Please provide a link to a minimal reproduction of the bug

Simply call AddTag as it should be called -->

public async Task<Tag> AddTags(int projectId, int screenshotId, IEnumerable<AddTagRequest> request)
{
    AddTagRequest[] requests = new AddTagRequest[]
    {
        new AddTagRequest
        {
            StringId = 328525 , // Valeur de l'identifiant de chaîne
            Position = new Position { X = 10, Y = 20, Height = 10, Width = 10 } // Valeurs de la position X et Y
        },
        // Ajoutez d'autres éléments AddTagRequest ici si nécessaire
    };
    return await _client.Screenshots.AddTag(projectId, screenshotId, request);
}

Please provide the exception or error you saw
Cannot deserialize the current JSON array (e.g. [1,2,3]) into type 'Crowdin.Api.Screenshots.Tag' because the type requires a JSON object (e.g. {"name":"value"}) to deserialize correctly.
To fix this error either change the JSON to a JSON object (e.g. {"name":"value"}) or change the deserialized type to an array or a type that implements a collection interface (e.g. ICollection, IList) like List that can be deserialized from a JSON array. JsonArrayAttribute can also be added to the type to force it to deserialize from a JSON array.
Path '', line 1, position 1.

Webhooks listing JSON parsing issue

Issue

Listing webhooks for a project throws an exception

Unexpected character encountered while parsing value: {. Path 'payload['project.approved'].project', line 15, position 18.
   at Newtonsoft.Json.JsonTextReader.ReadStringValue(ReadType readType)
   at Newtonsoft.Json.JsonTextReader.ReadAsString()
   at Newtonsoft.Json.JsonReader.ReadForType(JsonContract contract, Boolean hasConverter)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateDictionary(IDictionary dictionary, JsonReader reader, JsonDictionaryContract contract, JsonProperty containerProperty, String id)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateDictionary(IDictionary dictionary, JsonReader reader, JsonDictionaryContract contract, JsonProperty containerProperty, String id)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.SetPropertyValue(JsonProperty property, JsonConverter propertyConverter, JsonContainerContract containerContract, JsonProperty containerProperty, JsonReader reader, Object target)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject(Object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty member, String id)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)
   at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType)
   at Newtonsoft.Json.JsonConvert.DeserializeObject(String value, Type type, JsonSerializerSettings settings)
   at Newtonsoft.Json.JsonConvert.DeserializeObject[T](String value, JsonSerializerSettings settings)
   at Crowdin.Api.Core.JsonParser.ParseResponseObject[TData](JObject rootElement) in JsonParser.cs:line 38
   at System.Linq.Enumerable.SelectIListIterator`2.ToList()
   at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
   at Crowdin.Api.Core.JsonParser.ParseResponseList[TData](JObject rootElement) in JsonParser.cs:line 41
   at Crowdin.Api.Webhooks.WebhooksApiExecutor.<ListWebhooks>d__4.MoveNext() in WebhooksApiExecutor.cs:line 39
   at Program.<<Main>$>d__0.MoveNext()

which is caused by an invalid type for the payload property.

Cause

Given an example webhook JSON response (Crowdin's default payload):

[
    {
      "data": {
        "id": 9,
        "projectId": 7,
        "name": "File proofreading complete",
        "url": "https://something.com",
        "events": ["project.approved"],
        "headers": {
          "x-api-key": "secret"
        },
        "payload": {
          "project.approved": {
            "event": "{{event}}",
            "project": {
              "id": "{{projectId}}",
              "userId": "{{projectUserId}}",
              "sourceLanguageId": "{{projectSourceLanguageId}}",
              "targetLanguageIds": "{{projectTargetLanguageIds}}",
              "identifier": "{{projectIdentifier}}",
              "name": "{{projectName}}",
              "createdAt": "{{projectCreatedAt}}",
              "updatedAt": "{{projectUpdatedAt}}",
              "lastActivity": "{{projectLastActivity}}",
              "description": "{{projectDescription}}",
              "url": "{{projectUrl}}",
              "cname": "{{projectCname}}",
              "logo": "{{projectLogo}}",
              "isExternal": "{{projectIsExternal}}",
              "externalType": "{{projectExternalType}}",
              "hasCrowdsourcing": "{{projectHasCrowdsourcing}}"
            },
            "targetLanguage": {
              "id": "{{targetLanguageId}}",
              "name": "{{targetLanguageName}}",
              "editorCode": "{{targetLanguageEditorCode}}",
              "twoLettersCode": "{{targetLanguageTwoLettersCode}}",
              "threeLettersCode": "{{targetLanguageThreeLettersCode}}",
              "locale": "{{targetLanguageLocale}}",
              "androidCode": "{{targetLanguageAndroidCode}}",
              "osxCode": "{{targetLanguageOsxCode}}",
              "osxLocale": "{{targetLanguageOsxLocale}}",
              "textDirection": "{{targetLanguageTextDirection}}",
              "dialectOf": "{{targetLanguageDialectOf}}"
            }
          }
        },
        "isActive": true,
        "batchingEnabled": false,
        "requestType": "POST",
        "contentType": "application/json",
        "createdAt": "2023-05-05T10:22:40+02:00",
        "updatedAt": "2023-05-05T10:22:40+02:00"
      }
    }
  ]

and the CSharp model

public class Webhook
  {
// ...
    [JsonProperty("payload")]
    public IDictionary<string, IDictionary<string, string>> Payload { get; set; }
  }

as we can see, the property Payload is of type IDictionary<string, IDictionary<string, string>>. However, things get worse when we look at the JSON file property payload['project.approved'].project. JsonConverter expects it to be of type string however there is another Dictionary here.

Potential solution

The AddWebhook request accepts the Payload property just as object. Maybe using object type in the response would resolve that problem.

Or maybe in both cases, the Payload property should be of type string, accepting any object already serialized to a JSON string?

Distributions API changes

The Distribution creation API has been changed. Now you can create a distribution with the following parameters:

  • bundleIds - array of bundle ids

The following parameters have been deprecated:

  • format
  • exportPattern
  • labelIds

A Distribution response has also been changed. Now it contains the bundleIds field. The format, exportPattern, and labelIds fields are deprecated.

In addition, the fileIds request parameter is now optional.

All the changes are described in the API documentation.

BundleApiExector.ExportBundle responds 500 because it's not sending an expected Content-Type header.

Current:

The call BundleApiExector.ExportBundle() aka POST api/v2/projects/{projectId}/bundles/{bundleId}/exports doesn't require any content, the package doesn't send any as expected, however when using it, the API now returns the following;

Crowdin.Api.CrowdinApiException: Unsupported Content-Type Header
   at Crowdin.Api.CrowdinApiClient.CheckDefaultPreconditionsAndErrors(HttpResponseMessage response) in C:\Projects\crowdin-api-client-dotnet\src\Crowdin.Api\CrowdinApiClient.cs:line 416
   at Crowdin.Api.CrowdinApiClient.SendRequest(Func`1 createRequestMessage) in C:\Projects\crowdin-api-client-dotnet\src\Crowdin.Api\CrowdinApiClient.cs:line 354
   at Crowdin.Api.Bundles.BundlesApiExecutor.ExportBundle(Int32 projectId, Int32 bundleId) in C:\Projects\crowdin-api-client-dotnet\src\Crowdin.Api\Bundles\BundlesApiExecutor.cs:line 119
   at OSM.Translations.Api.Services.CrowdinService.ExportBundle(Bundle bundleDefinition) in C:\Projects\OSM.Translations\OSM.Translations.Api\Services\CrowdinService.cs:line 50
   at OSM.Translations.Api.Services.CrowdinService.GetBundleDownloadUrl(String bundleName) in C:\Projects\OSM.Translations\OSM.Translations.Api\Services\CrowdinService.cs:line 32
   at OSM.Translations.Api.V1.TranslationController.GetCrowdinTranslationBundleAsDownloadableUrl(String bundleName) in C:\Projects\OSM.Translations\OSM.Translations.Api\V1\TranslationController.cs:line 31
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
   at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

HEADERS
=======
Accept: application/json
Host: localhost:59954
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36
:method: GET
Accept-Encoding: gzip, deflate, br
Accept-Language: en,nl;q=0.9,nl-NL;q=0.8
Referer: https://localhost:59954/swagger/index.html
sec-ch-ua: "Not.A/Brand";v="8", "Chromium";v="114", "Google Chrome";v="114"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
sec-fetch-site: same-origin
sec-fetch-mode: cors
sec-fetch-dest: empty

Expected:
Either the API shouldn't expect the Content-Type field on this call, or it should be set through the package (which would require creating JsonContent even though none is included).

.NET 5.0 Out of Support

Hi,

I saw that that projects Crowdin.Api.Samples.csproj and Crowdin.Api.Tests.csproj are still using .NET 5.0 version.
As support for .NET 5.0 version is already ended back in May, 2022. 🔗

I suggest these projects can be migrated to current .NET 6.0 version.

Wrong parameter type in TranslationStatusApiExecutor.GetLanguageProgress

As far as I can tell, Crowdin.Api.TranslationStatus.TranslationStatusApiExecutor.GetLanguageProgress is the way to reference the Get Language Progress call listed in the API reference. However, the API reference requires a language ID in the form of a string (e.g. "en", "es", etc.), but here it is defined as an int which doesn't seem to have any correlation to what the API actually expects.

Am I missing something?

invalid Request class member `ExportApprovedOnly` for `ExportProjectTranslation`

I tried to call API ExportProjectTranslation with below code, but got response error.

            DownloadLink exportedLink = await apiClient.Translations.ExportProjectTranslation("ProjectId",
                new ExportProjectTranslationRequest()
                {
                    TargetLanguageId = "en",
                    ExportApprovedOnly = true, // seems invalid paramter for current Crowdin API Service.
                });

and error detail:

Url https://@@@@.crowdin.com/api/v2/projects/4/translations/exports
Method POST
Status Error
Message Bad Request
Authorization Type Personal Access Token

Request details | { "targetLanguageId": "en", "exportApprovedOnly": true }
Errors | { "errors": [ { "error": { "key": "exportApprovedOnly", "errors": [ { "code": "notFound", "message": "Field 'exportApprovedOnly' Not Found" } ] } } ] }

Please check this issue.

Missing Project webhook types

During my attempt to implement automated project webhooks management, I discovered that some of the events are missing in the file:

    [PublicAPI]
    public enum EventType
    {
        [Description("file.translated")]        FileTranslated,
        [Description("file.approved")]        FileApproved,
        [Description("project.translated")]        ProjectTranslated,
        [Description("project.approved")]        ProjectApproved,
        [Description("translation.updated")]        TranslationUpdated,
        [Description("string.added")]        StringAdded,
        [Description("string.updated")]        StringUpdated,
        [Description("string.deleted")]        StringDeleted,
        [Description("suggestion.added")]        SuggestionAdded,
        [Description("suggestion.updated")]        SuggestionUpdated,
        [Description("suggestion.deleted")]        SuggestionDeleted,
        [Description("suggestion.approved")]        SuggestionApproved,
        [Description("suggestion.disapproved")]        SuggestionDisapproved
    }

The missing ones are for instance: file.added or file.updated.

Is there a particular reason why they were not included in the client?

`ExportProjectTranslation`: CrowdinApiException: Invalid Request Parameters: Key [format]: Value is required and can't be empty

Hi,

I tried to call the ExportProjectTranslation API with the below code:

var downloadLink = await _crowdin.Translations.ExportProjectTranslation(projectId, new ExportProjectTranslationRequest()
{
    TargetLanguageId = langId,
});

I got this error:
image

Note that, I want to get the original file format so I didn't pass Format property (according to https://developer.crowdin.com/api/v2/#operation/api.projects.translations.exports.post):

Note: the format parameter is required in all cases except when you'd like to export translations for a single file in its original format

I also tried passing the format, but the TranslationFormat enum does not have the "original" option.

Please help, thank you!

C# API Client: StringBatchOperations doesn't work

Is this a regression?
No. StringBatchOperations wasn't available in 2.12

In which version?
2.14

Description
We're currently working on the C# Crowdin API’s StringBatchOperations and we faced the following issue:

{ "ClassName": "System.NullReferenceException", "Message": "Object reference not set to an instance of an object.", "StackTraceString": " at Crowdin.Api.Core.JsonParser.ParseResponseObject[TData](JToken token)\r\n at Crowdin.Api.Core.JsonParser.ParseResponseList[TData](JObject rootElement)\r\n at Crowdin.Api.SourceStrings.SourceStringsApiExecutor.StringBatchOperations(Int32 projectId, IEnumerable1 patches)\r\n", }

When debugging in the SourceStringsApiExecutor.cs, we found that the http response from CrowdinAPI has the status 200 OK and is returning the following jsonObject:

{ "data": [ { ""data"": { ""id"": 468549, ""projectId"": 11, ""fileId"": 1533, ""branchId"": null, ""directoryId"": 1535, ""identifier"": ""bulk_edit_6"", ""text"": ""Batch operations with send patch request"", ""type"": ""text"", ""context"": ""edited context using patch request"", ""maxLength"": 0, ""isHidden"": false, ""isDuplicate"": false, ""masterStringId"": null, ""revision"": 58, ""hasPlurals"": false, ""isIcu"": false, ""labelIds"": [], ""createdAt"": ""2023-06-05T12:00:32+02:00"", ""updatedAt"": ""2023-06-05T18:38:26+02:00"" } } ] }

As you can see, the response is missing the pagination object (like mentioned in CrowdinAPI) which is probably why the line return _jsonParser.ParseResponseList<SourceString>(crowdinApiResult.JsonObject); in SourceStringsApiExecutor.cs fails to parse it and triggers the NullReferenceException.

Please provide a link to a minimal reproduction of the bug

A simple call of StringBatchOperations.

Please provide the exception or error you saw

{ "ClassName": "System.NullReferenceException", "Message": "Object reference not set to an instance of an object.", "Data": null, "InnerException": null, "HelpURL": null, "StackTraceString": " at Crowdin.Api.Core.JsonParser.ParseResponseObject[TData](JToken token)\r\n at Crowdin.Api.Core.JsonParser.ParseResponseList[TData](JObject rootElement)\r\n at Crowdin.Api.SourceStrings.SourceStringsApiExecutor.StringBatchOperations(Int32 projectId, IEnumerable1 patches)\r\n", "RemoteStackTraceString": null, "RemoteStackIndex": 0, "ExceptionMethod": null, "HResult": -2147467261, "Source": "Crowdin.Api", "WatsonBuckets": null }

Parse failed when calling ListLanguageTranslations

Hello,
When calling ListLanguageTranslations, parse failed about LanguageTranslations, it's an abstract class.

It can be patched in StringTranslationsApiExecutor.cs
public Task<ResponseList<T>> ListLanguageTranslations<T>( int projectId, string languageId, string? stringIds = null, string? labelIds = null, int? fileId = null, string? croql = null, bool? denormalizePlaceholders = null, int limit = 25, int offset = 0) where T : LanguageTranslations { return ListLanguageTranslations<T>(projectId, languageId, new LanguageTranslationsListParams(stringIds, labelIds, fileId, croql, denormalizePlaceholders, limit, offset)); }
public async Task<ResponseList<T>> ListLanguageTranslations<T>( int projectId, string languageId, LanguageTranslationsListParams @params) where T : LanguageTranslations { var url = $"/projects/{projectId}/languages/{languageId}/translations"; CrowdinApiResult result = await _apiClient.SendGetRequest(url, @params.ToQueryParams()); return _jsonParser.ParseResponseList<T>(result.JsonObject); }

Fix ProjectInviteUrls for ProjectInfo

Project Details API

If project language access policy is "Open" the invite URL section will look like the following:

<invite_url>
  <translator>
    https://crowdin.com/project/test-project/invite
  </translator>
  <proofreader>
    https://crowdin.com/project/test-project/invite?d=95l625i6r4v6p483v6q4r44303
  </proofreader>
</invite_url>

But if project language access policy is "Moderate" (Limited access to the languages), invite_url has the following structure:

<invite_url>
    <item>
      <language>Ukrainian</language>
      <translator>
        https://crowdin.com/project/test-project/invite?d=d5l6k4h685v6p483v6q4r44303
      </translator>
      <proofreader>
        https://crowdin.com/project/test-project/invite?d=95l625i6r4v6p483v6q4r44303
      </proofreader>
    </item>
</invite_url>

In this case program crashes with error:

Unhandled exception. System.InvalidOperationException: There is an error in XML document (68, 7).
 ---> System.InvalidOperationException: There is an error in XML document (68, 7).
 ---> System.Xml.XmlException: Missed expected element 'translator' Line 68, position 7.
......

The problem is in ProjectInviteUrls.cs file, ReadXml method - it expects as required 'translator' and 'proofreader' nodes and does not expect item nodes.

HttpRequestMessage retries issue and HttpClient lifetime

Background

HttpRequestMessage instance is reused during retries which throws an exception.
HttpClient lifetime is equal to the lifetime of the entire CrowdinApiClient.

Issue

When retries mechanism is enabled

System.InvalidOperationException
  Message=The request message was already sent. Cannot send the same request message multiple times.
  Source=System.Net.Http
  StackTrace:
   at System.Net.Http.HttpClient.CheckRequestMessage(HttpRequestMessage request)
   at System.Net.Http.HttpClient.CheckRequestBeforeSend(HttpRequestMessage request)
   at Crowdin.Api.CrowdinApiClient.<>c__DisplayClass90_0.<SendRequest>b__0()
   at Crowdin.Api.Core.Resilience.RetryService.<ExecuteRequestAsync>d__2`1.MoveNext()
   at Crowdin.Api.Core.Resilience.RetryService.<ExecuteRequestAsync>d__2`1.MoveNext()
   at Crowdin.Api.CrowdinApiClient.<SendRequest>d__90.MoveNext()
   at Crowdin.Api.Languages.LanguagesApiExecutor.<ListSupportedLanguages>d__5.MoveNext()
   at Crowdin.Api.CrowdinApiClient.<WithFetchAll>d__89`1.MoveNext()
   at Program.<>c__DisplayClass0_0.<<<Main>$>g__GetSupportedLanguages|5>d.MoveNext()

Test environment

  • .NET 6 runtime
  • Crowdin.Api v2.11.1

Reproduction

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Crowdin.Api" Version="2.11.1" />
    <PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
  </ItemGroup>

</Project>
var credentials = new CrowdinCredentials
{
    AccessToken = "secret",
    Organization = "test-org",
    BaseUrl = "http://localhost:58190" // some invalid Url on purpose, to make it fail
};

var client = new CrowdinApiClient(
    credentials,
    retryService: new RetryService(new RetryConfiguration
    {
        RetriesCount = 5,
        WaitIntervalMilliseconds = 1500
    })
);

var languages = await CrowdinApiClient
.WithFetchAll((limit, offset) => client.Languages.ListSupportedLanguages(limit, offset), amountPerRequest: 500);

Workaround

The only workaround for this moment to make retries work is to define external policy and wrap the entire process of CrowdinApiClient instance creation and sending a single request as one.

Solution

I've come up with a solution for this issue in this PR.
Creating a new instance of the HttpClient and a new instance of the HttpRequestMessage.
That solved the issue.

It's a breaking change due to changes in the constructor of the CrowdinApiClient.

It also resolves another issue. The HttpClient instance should be used only for a very small period of time. In the suggested solution the HttpClient instance is created before every API call.

An additional benefit is that now the developers can use IHttpClientFactory to create new instances of the HttpClient.

Testing

This code can't be really unit tested. I only managed to test it locally with API calls.

Future improvements

The signature of the retry method can be improved.

From

public interface IRetryService
    {
        Task<T> ExecuteRequestAsync<T>(Func<Task<T>> func);
    }

Into

public interface IRetryService
    {
        Task<HttpResponseMessage> ExecuteRequestAsync(Func<Task<HttpResponseMessage>> func);
    }

This will allow more advanced users to define their own Retry services e.g. using Polly thanks to the HttpResponseMessage publicly exposed in the method's signature.

BundleExporter.CheckBundleExportStatus cannot deserialize status "in_progress" properly.

When the BundleExporter.CheckBundleExportStatus returns a status of "in_progress" we run into the following JSON deserialization issue;

System.ArgumentException: Error occurred during deserialization from JSON String
   at Crowdin.Api.Core.Converters.DescriptionEnumConverter.ReadJson(JsonReader reader, Type objectType, Object existingValue, JsonSerializer serializer) in C:\Projects\crowdin-api-client-dotnet\src\Crowdin.Api\Core\Converters\DescriptionEnumConverter.cs:line 85
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.DeserializeConvertable(JsonConverter converter, JsonReader reader, Type objectType, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.SetPropertyValue(JsonProperty property, JsonConverter propertyConverter, JsonContainerContract containerContract, JsonProperty containerProperty, JsonReader reader, Object target)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject(Object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty member, String id)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)
   at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType)
   at Newtonsoft.Json.JsonConvert.DeserializeObject(String value, Type type, JsonSerializerSettings settings)
   at Newtonsoft.Json.JsonConvert.DeserializeObject[T](String value, JsonSerializerSettings settings)
   at Crowdin.Api.Core.JsonParser.ParseResponseObject[TData](JObject rootElement) in C:\Projects\crowdin-api-client-dotnet\src\Crowdin.Api\Core\JsonParser.cs:line 53
   at Crowdin.Api.Bundles.BundlesApiExecutor.CheckBundleExportStatus(Int32 projectId, Int32 bundleId, String exportId) in C:\Projects\crowdin-api-client-dotnet\src\Crowdin.Api\Bundles\BundlesApiExecutor.cs:line 133
   at OSM.Translations.Api.Services.CrowdinService.ExportBundle(Bundle bundleDefinition) in C:\Projects\OSM.Translations\OSM.Translations.Api\Services\CrowdinService.cs:line 55
   at OSM.Translations.Api.Services.CrowdinService.GetBundle(String bundleName) in C:\Projects\OSM.Translations\OSM.Translations.Api\Services\CrowdinService.cs:line 21
   at OSM.Translations.Api.V1.TranslationController.GetCrowdinTranslationsBundleAsZip(String bundleName) in C:\Projects\OSM.Translations\OSM.Translations.Api\V1\TranslationController.cs:line 23
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
   at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

HEADERS
=======
Accept: application/json
Host: localhost:59954
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36
:method: GET
Accept-Encoding: gzip, deflate, br
Accept-Language: en,nl;q=0.9,nl-NL;q=0.8
Referer: https://localhost:59954/swagger/index.html
sec-ch-ua: "Not.A/Brand";v="8", "Chromium";v="114", "Google Chrome";v="114"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
sec-fetch-site: same-origin
sec-fetch-mode: cors
sec-fetch-dest: empty

Expected: It should properly parse the status to BuildStatus.InProgress.

Reason: The Enum definition has a nice Description attribute above it which is read by a custom deserializer, but it is initialized with inProgress instead of in_progress (perhaps because it has been changed recently?)

Translation Status: new `preTranslateAppliedTo` field in Progress API Methods' response

Crowdin has recently enhanced its response for the Progress API Methods:

For example:

{
  "data": [
    {
      "data": {
        "words": {
          "total": 7249,
          "translated": 3651,
+          "preTranslateAppliedTo": 1254,
          "approved": 3637
        },
        "phrases": {
          "total": 3041,
          "translated": 2631,
+          "preTranslateAppliedTo": 1254,
          "approved": 2622
        },
        "translationProgress": 86,
        "approvalProgress": 86,
        "languageId": "af"
      }
    }
  ],
  "pagination": {
    "offset": 0,
    "limit": 25
  }
}

The response of these methods now contains a new field preTranslateAppliedTo in the words and phrases objects.

These new fields should also be reflected in this API Client.

New source files Import Options

Crowdin Rest API Add File and Update or Restore File now support new ImportOptions types:

  • Html File Import Options
  • Html with Front Matter File Import Options
  • Mdx v1 File Import Options

These new Import Options should be added to this API client as well as the corresponding unit tests.

Bug in Translatable Elements XPath

When TranslatableElements = "/strings//group/string[@txt]", elements matching /strings/group/string[@txt] should be translatable in the Crowdin web interface, but are not. For instance, in an XML file containing

<?xml version="1.0" encoding="UTF-8"?>
<strings>
  <group id="TopGroup1">
    <string txt="No Records"/>
  </group>
  <group id="TopGroup2">
    <group id="SubGroup1">
      <string txt="Some Text"/>
    </group>
    <group id="SubGroup2">
      <group id="SubSubGroup">
        <string txt="Anywhere"/>
      </group>
    </group>
  </group>
</strings>

all txt attributes should be translatable, but only those under subgroups are (Some Text, Anywhere) and those under the top-level group are not (No Records).

code snippet:

var httpClient = new HttpClient { BaseAddress = new Uri("https://api.crowdin.com/api/") };
var crowdinClient = new Client(httpClient);
var files = new Dictionary<string, FileInfo>
{
	["sample.xml"] = new FileInfo("sample.xml")
};
var addFileParams = new AddFileParameters
{
	Files = files,
	TranslatableElements = new[] {"/strings//group/string[@txt]"}
};
await crowdinClient.AddFile(projectId, credentials, addFileParams);

Closed Stream exception when calling AddStorage

Hello,
sometimes AddStorage fails due to the stream that is disposed before the end of the upload task.
I think the stream should not be disposed by the AddStorage, but by the creator of the stream.

Patch in CrowdinApiClient.cs : remove the using clause

       public Task<CrowdinApiResult> UploadFile(string subUrl, string filename, Stream fileStream)
        {
            //using Stream stream = fileStream;

            var request = new HttpRequestMessage
            {
                Method = HttpMethod.Post,
                Content = new StreamContent(fileStream),
                RequestUri = new Uri(FormRequestUrl(subUrl)),
            };
            request.Headers.Add("Crowdin-API-FileName", filename);
            request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");

            return SendRequest(request);
        }

then you could dispose the stream in caller (StorageApiExecutor.cs) like this :

        [PublicAPI]
        public async Task<StorageResource> AddStorage(Stream fileStream, string filename)
        {
            using var stream = fileStream;
            CrowdinApiResult result = await _apiClient.UploadFile(BaseSubUrl, filename, stream);
            return _jsonParser.ParseResponseObject<StorageResource>(result.JsonObject);
        }

or my prefered method : let the creator of the stream dispose the stream, example :

            using (var St = System.IO.File.OpenRead(Source_File))
            {
                var Storage = await API.Storage.AddStorage(St, CNT.source);
            }

Webhooks headers types inconsistency

In Webhook response model, the Headers property is of type Dictionary:

public class Webhook
  {
    //...
    [JsonProperty("headers")]
    public IDictionary<string, string> Headers { get; set; }
  }

however in the request model AddWebhookRequest, it is just of type object:

public class AddWebhookRequest
  {
    //...
    [JsonProperty("headers")]
    public object? Headers { get; set; }
  }

Publish a Nuget package of the Crowdin.Api

A Nuget package of the Crowdin.Api Project would be very useful.

I am building a tool based on the Crowdin.Api and it would be nice if the very good code that is sitting in develop was merged to master and a Nuget package published.

Strict validation of the Content-Type header

Background

The CrowdinApiClient class contains a very strict validation when it comes to the Content-Type response header.

https://github.com/crowdin/crowdin-api-client-dotnet/blob/main/src/Crowdin.Api/CrowdinApiClient.cs#L323

private static readonly MediaTypeHeaderValue DefaultContentType = MediaTypeHeaderValue.Parse("application/json");
// ...
if (response.Content.Headers.ContentType.Equals(DefaultContentType))

I faced an issue with this validation while setting up a mock API for integration tests in my system.

Most of the modern WebApis return the header in such a way Content-Type: application/json; charset=utf-8.
This header value does not match the validation.

Solution

The solution seems pretty easy to develop. Easing the validation should not break any of the working parts of the client.

Unit tests

It would be good to write some Unit tests for the classes of this client.
Write tests for most important parts of src/Crowdin.Api.

Webhook.AddWebhook endpoint response serialization error

When calling Webhooks.AddWebhook, I'm getting the following error:

Newtonsoft.Json.JsonSerializationException: Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'System.String[]' because the type requires a JSON array (e.g. [1,2,3]) to deserialize correctly.

When I checked webhook via UI, the Webhook integration was there. The endpoint creates the webhook but cannot serialize the response. there are two properties which are headers and payload, those properties are tiring to cast to a string array but they are objects.

Related documentation is here

For reference, this is the response I got from the server:

{
  "data": {
    "id": 37,
    "projectId": 15,
    "name": "Backend-Integration",
    "url": "https://test.com/apiservice/translation/file-translated",
    "events": [
      "file.translated"
    ],
    "headers": {
      "auth": "token"
    },
    "payload": {
      "file.translated": {
        "eventType": "{{event}}",
        "projectId": "{{projectId}}",
        "fileId": "{{fileId}}",
        "targetLanguageId": "{{targetLanguageId}}"
      }
    },
    "isActive": true,
    "batchingEnabled": false,
    "requestType": "POST",
    "contentType": "application/json",
    "createdAt": "2022-09-19T17:22:23+03:00",
    "updatedAt": "2022-09-19T17:22:23+03:00"
  }
}

this is my pr request if you interested

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.