Repository navigation
feat: add Advisors, Placeholders and Organization APIs; harden asset translation detection - #434
Merged
Conversation
…translations Unknown language translation types are read as assets only when contentType is a non-empty string outside Crowdin's application/vnd.crowdin.* text types and the item has a url without text or plurals. Other unknown shapes stay rejected. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Advisors: run and get advisor checks, list and edit insights, upsert an
application module insight
- Placeholders: system placeholders (list, batch edit), organization custom
placeholders and project placeholders (list, add, get, edit, delete)
- Organization: get organization and auth settings, custom spellcheckers,
external QA checks list
- Organization.isVendor accepts a boolean or a string and keeps the raw value
- GET /external-qa-checks/{id} is not added: the spec documents a collection
response for a single resource
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
setIsVendor also sets the raw value ("true" / "false"), setIsVendorRaw keeps
the raw string and recomputes the boolean with the same rules as the JSON
reader, and null clears both fields. Signatures and JSON output are unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
andrii-bodnar
approved these changes
Nov 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the Advisors, Placeholders and Organization APIs from the current Crowdin OpenAPI specifications (crowdin + enterprise, file-based + string-based). It also hardens the asset-translation classifier added in #433. All changes are additive and keep source and binary compatibility with the latest release, 1.37.0.
Coverage: 22 of the 23 non-deprecated Advisors / Placeholders / Organization routes. It is not complete:
GET /external-qa-checks/{externalQaCheckId}is the one remaining gap (see below).The PR has three commits:
fix(translations): the classifier hardening, limited to the deserializer and its tests.feat: the three new APIs, wired intoClientasgetAdvisorsApi(),getPlaceholdersApi()andgetOrganizationApi().fix(organization):isVendor/isVendorRawsetters kept in sync.New API methods
Advisors (
AdvisorsApi, all 4 editions)runAdvisorCheck(projectId, RunAdvisorCheckRequest)POST /projects/{projectId}/advisors/checksgetAdvisorCheck(projectId, String checkId)GET /projects/{projectId}/advisors/checks/{checkId}listAdvisorInsights(projectId, ListAdvisorInsightsParams)GET /projects/{projectId}/advisors/insightseditAdvisorInsight(projectId, insightId, List<PatchRequest>)PATCH /projects/{projectId}/advisors/insights/{insightId}upsertApplicationAdvisorInsight(projectId, applicationIdentifier, moduleKey, UpsertAdvisorInsightRequest)PUT /projects/{projectId}/applications/{applicationIdentifier}/modules/{moduleKey}/advisors/insightsupsertApplicationAdvisorInsightreturnsvoid, because both success responses (200 updated, 201 created) have no body.Placeholders (
PlaceholdersApi)listSystemPlaceholders(projectId, limit, offset)GET /projects/{projectId}/system-placeholderseditSystemPlaceholders(projectId, List<PatchRequest>)PATCH /projects/{projectId}/system-placeholderslistCustomPlaceholders(limit, offset)GET /custom-placeholdersaddCustomPlaceholder(AddOrganizationPlaceholderRequest)POST /custom-placeholdersgetCustomPlaceholder(id)/editCustomPlaceholder(id, patch)/deleteCustomPlaceholder(id)listProjectPlaceholders(projectId, limit, offset)GET /projects/{projectId}/placeholdersaddProjectPlaceholder(projectId, AddProjectPlaceholderRequest)POST /projects/{projectId}/placeholdersgetProjectPlaceholder/editProjectPlaceholder/deleteProjectPlaceholderSystem, organization custom and project placeholders are separate models (
SystemPlaceholder,OrganizationPlaceholder,ProjectPlaceholder). The deprecated AI custom-placeholder endpoints (/ai/settings/custom-placeholders,/users/{userId}/ai/settings/custom-placeholders) are already in the releasedAIApi. They are not duplicated or extended here.Organization (
OrganizationApi, Enterprise)getOrganization()GET /organizationgetOrganizationAuthSettings()GET /organization/auth-settingslistCustomSpellcheckers(limit, offset)GET /custom-spellcheckersgetCustomSpellchecker(id)GET /custom-spellcheckers/{id}listExternalQaChecks(projectId, limit, offset)GET /external-qa-checksIntentionally omitted:
GET /external-qa-checks/{externalQaCheckId}(approved; the one remaining coverage gap)The spec ("Get External QA Check",
api.external-qa-checks.get) documents the response asExternalQaCheckCollectionResource, the same schema as the list endpoint.datais an array, even though the URL addresses one resource. Both Enterprise editions say the same, and there is no response example to settle it. Returning a single object would contradict the documented contract, and returning a list for a single-resource GET would likely be wrong. So no method or model is added. Maintainer decision (approved): keep this endpoint unimplemented until Crowdin confirms the response shape.To implement it later, Crowdin needs to confirm whether the live response is
{"data": {…}}(single object) or{"data": [{"data": {…}}]}(collection), ideally with a real response example in the docs. With a single object, the method would beResponseObject<ExternalQaCheck> getExternalQaCheck(Long externalQaCheckId), reusing the existingExternalQaCheckResponseObject.Organization.isVendordecisionEvidence: both Enterprise specs declare
isVendoras{"type": "string", "example": false}. The type is string, but the schema's own example is a boolean, andGET /organizationhas no response example.Implementation: the SDK accepts both shapes without guessing or losing data.
Boolean isVendoris set from a JSON boolean, or from the strings"true"/"false"(case-insensitive). Any other value leaves itnull; nothing is converted from"1","yes"and the like.String isVendorRawalways keeps the raw value as text, e.g."false"or"vendor", so an unexpected string is never lost.This follows the dual-field pattern of
excludedElementsin #430. Tests cover JSON booleans, boolean strings, unrecognized strings, numbers,null, missing, and serialization.Maintainer decision (approved): keep both
isVendorandisVendorRawfor now, because the specification contradicts its own example. If Crowdin confirms the live API returns a boolean,isVendorRawsimply stays redundant.Setters (resolved). Both setters are now explicit and keep the two fields in sync. Their signatures are identical to the Lombok-generated ones.
setIsVendor(Boolean)sets the raw value to"true"/"false".setIsVendorRaw(String)keeps the string exactly as given (e.g."TRUE","vendor") and recomputesisVendorwith the same rule as the JSON reader. Unknown strings givenull.nullpassed to either setter clears both fields.Remaining limitation: serialization normalizes the shape.
"true"/"false"strings are written back as JSON booleans, and unexpected non-string values (numbers, objects) are written back as strings. The meaning is preserved and there is always exactly oneisVendorkey.Asset translation classifier hardening (follow-up to #433)
An unknown List Language Translations
contentTypeis now read asAssetLanguageTranslationsonly when all of these hold:contentTypeis a non-empty string.null,"", whitespace, numbers and booleans are now rejected; objects and arrays were already rejected.application/vnd.crowdin.*text types.url, withouttextorplurals.Unchanged:
text/plain, plural and ICU translations map exactly as before.Wrong type: …, and a missingcontentTypestill throwsCouldn't get class.Remaining ambiguity (documented by a test):
text/markdownwith aurland notextis still read as an asset.text/*is not excluded, because the API doesn't state that an asset can never have atext/*MIME type.Validation
AdvisorsApiTest(5),PlaceholdersApiTest(12),OrganizationApiTest(6),OrganizationIsVendorTest(10, including setter call orders, sync, and setter/JSON-reader agreement).LanguageTranslationsDeserializationTest(11, of which 6 are new classifier regression tests).StringTranslationsApiTest(44, unchanged)../gradlew clean buildpasses on JDK 17 and JDK 8. JDK 8 Javadoc: 0 errors.GET /external-qa-checks/{id}. Path parameter types match the spec: integers asLong;checkId,applicationIdentifierandmoduleKeyasString.Organization.isVendor, which the script can't see because it's read via@JsonSetter; it's covered byOrganizationIsVendorTest.nullarguments.Known limitations and unverified contracts (spec only, no live check)
GET /external-qa-checks/{id}is omitted (see above).Organization.isVendor: the live type is unconfirmed (see above).text/*assets can't be told apart from unknown text types that have aurland notext.Maintainer decisions
GET /external-qa-checks/{id}unimplemented until Crowdin confirms whether the response is a single object or a collection. This is the one remaining coverage gap (22/23).Organization.isVendorandOrganization.isVendorRawfor now.Optional follow-up: setters that keepResolved inisVendorandisVendorRawin sync.fix(organization).🤖 Generated with Claude Code