External QA Check Module
Este conteúdo não está disponível em sua língua ainda.
Translate in CrowdinThis module allows for the integration of advanced, AI-powered, and other specialized QA checks, enabling verification of translations for nuanced issues that cannot be detected by default QA checks or JavaScript-based Custom QA checks.
Crowdin Enterprise sends translations to the app in batches over HTTP, and the app answers whether each translation passes the check. A check can run in one of two modes:
- Asynchronous (default): The app is called in the background after a translation is saved. The result appears in the Editor a few seconds or minutes later, and it never prevents a translation from being saved.
- Synchronous: The app is also called while a translation is being saved, so translators see the result right away. A project can set a synchronous check to block translations that fail it.
You can grant access to this module to one of the following user categories:
- Only organization admins
- All users in the organization projects
- Selected users
{ "modules": { "external-qa-check": [ { "key": "custom-check-qa", "name": "QA Check", "description": "Description", "runQaCheckUrl": "/validate", "getBatchSizeUrl": "/batch-size", "supportsSynchronousRun": true, "url": "/settings/index.html" } ] }}An app can declare several external-qa-check modules. Each module becomes a separate QA check.
key | Type: Required: yes Description: Module identifier within the Crowdin app. |
name | Type: Required: yes Description: The human-readable name of the module. |
description | Type: Description: The human-readable description of what the module does. |
runQaCheckUrl | Type: Required: yes Description: The relative URL triggered when sending texts for QA validation. Crowdin sends a |
getBatchSizeUrl | Type: Required: no Description: The relative URL triggered when retrieving the batch size supported by the module. |
supportsSynchronousRun | Type: Required: no Default: Description: Set to |
url | Type: Required: no Description: The relative URL to the module settings page. |
environments | Type: Allowed values: Description: Set of environments where a module could be installed. |
Communication between External QA Check App and Crowdin
Section titled “Communication between External QA Check App and Crowdin”Crowdin sends translations for QA validation to runQaCheckUrl as a POST request with a JSON body. The app checks the translations and answers in the same HTTP response, either without QA issues or with QA issues.
- Each request contains translations for one check, one target language, and one file (or no file in string-based projects).
- The request contains the
Authorization: Bearer <JWT>header. The token identifies the organization and the app. It doesn’t identify the project or the user; these are in the request body. Verify the token with the app’s client secret. Read more about Crowdin Apps security. - The batch size is requested with a
GETrequest togetBatchSizeUrlwith the same header. See Batching.
Request to the App from Crowdin for runQaCheckUrl
Section titled “Request to the App from Crowdin for runQaCheckUrl”Request payload example:
{ "data": { "translations": [ { "id": 12345, "stringId": 1234567, "languageId": "fr", "userId": 1, "text": "La mise à jour est installé avec succès.", "provider": null, "pluralCategoryName": null, "isPreTranslated": false } ], "strings": [ { "id": 1234567, "key": "update_success", "context": "Confirmation of successful software update", "maxLength": null, "createdAt": "2026-09-01T10:00:00+00:00", "updatedAt": null, "text": "The update was successfully installed.", "fields": [] } ],57 collapsed lines
"sourceLanguage": { "id": "en", "name": "English", "twoLettersCode": "en", "threeLettersCode": "eng", "locale": "en-US", "pluralCategoryNames": ["one", "other"], "pluralRules": "(n != 1)", "pluralExamples": [1, 2], "textDirection": "ltr", "dialectOf": null }, "targetLanguage": { "id": "fr", "name": "French", "twoLettersCode": "fr", "threeLettersCode": "fra", "locale": "fr-FR", "pluralCategoryNames": ["one", "other"], "pluralRules": "(n > 1)", "pluralExamples": [1, 2], "textDirection": "ltr", "dialectOf": null }, "project": { "id": 123, "type": 0, "sourceLanguage": { "id": "en", "name": "English", "twoLettersCode": "en", "threeLettersCode": "eng", "locale": "en-US", "pluralCategoryNames": ["one", "other"], "pluralRules": "(n != 1)", "pluralExamples": [1, 2], "textDirection": "ltr", "dialectOf": null }, "targetLanguages": [ { "id": "fr", "name": "French", "twoLettersCode": "fr", "threeLettersCode": "fra", "locale": "fr-FR", "pluralCategoryNames": ["one", "other"], "pluralRules": "(n > 1)", "pluralExamples": [1, 2], "textDirection": "ltr", "dialectOf": null } ], "name": "Project Name", "description": "Project Description", "fields": [] }, "file": { "id": 123, "name": "filename.csv", "title": null, "context": null, "type": "csv", "path": "/filename.csv", "fields": [] } }}Notes on the request payload:
translations[].provideris the name of the MT, TM, or AI engine that produced the translation, ornullfor a manual translation or an upload.- For a plural string,
strings[].textis an object with one text per plural form, andtranslationscontains one item per plural form, each with itspluralCategoryName. - The
fileobject is sent only in file-based projects. - Some fields, such as
strings[].originalText, are sent only when they have a value. Treat fields that aren’t shown in the example as optional. - Empty translations aren’t sent.
Expected Response from the App (Without QA issues)
Section titled “Expected Response from the App (Without QA issues)”Response payload example:
{ "data": { "validations": [ { "translationId": 123, "passed": true } ] }}Expected Response from the App (With QA issues)
Section titled “Expected Response from the App (With QA issues)”Response payload example:
{ "data": { "validations": [ { "translationId": 456, "passed": false, "error": { "message": "Example error message", "details": { "rule": "brand-names" }, "suggestedFixes": [ { "indexStart": 0, "indexEnd": 6, "replacement": "Crowdin" } ] } } ] }}error.message(required): The text shown to the translator in the Editor. Use plain text, because HTML isn’t rendered. For a blocking check, the message is also returned in the API error.error.details(optional): An object with any additional data. It is stored with the QA issue.error.suggestedFixes(optional): Fixes offered to the translator in the Editor.indexStartandindexEndare positions in the translation text, andreplacementis the text to put there.
Crowdin rejects the whole response as invalid if:
- The body isn’t JSON, or it isn’t exactly
{"data": {"validations": [...]}}. - Any object contains a key that isn’t shown in the examples above. For example, don’t add
"error": nullto a passed translation, and don’t add a top-levelerrornext todata. translationIdisn’t an integer, orpassedisn’t a boolean.- A translation with
"passed": falsehas noerrorobject. - A translation from the request has no validation.
Validations with IDs that weren’t in the request are ignored.
An invalid response is handled the same way as an unavailable app. See Error Handling.
Response from the App to Crowdin for getBatchSizeUrl
Section titled “Response from the App to Crowdin for getBatchSizeUrl”Response payload example:
{ "data": { "size": 10 }}If the response doesn’t match this example, Crowdin uses the default batch size of 500.
When the app is installed, each external-qa-check module appears as a separate check in the project QA settings. A project manager enables it in Settings > Quality assurance. The check runs only when QA checks are enabled in the project.
For each enabled external check, a project manager chooses its severity:
- Warning: Translators see the QA issue and can still save the translation with Save Anyway. Asynchronous checks always use this severity.
- Error: A translation that fails the check isn’t saved. Available only for checks with
supportsSynchronousRun: true. When you update a project using the Edit Project API method, add the check ID toexternalQaCheckIdsand set its value inexternalQaChecksIgnorabletofalseto make it blocking.
If an app update removes supportsSynchronousRun, the check stops blocking translations in all projects and works as an asynchronous check.
Changes to the module in an app update (for example, new URLs or a new supportsSynchronousRun value) take effect within about a minute. An app update doesn’t re-check existing translations. To re-check them, see Revalidation.
Every enabled external check, synchronous or not, is called in the background when:
- A translation becomes the top translation of a string (for example, after a new translation, an approval, a vote, or when the previous top translation is deleted).
- An existing translation is edited.
- The source text or the maximum length of a string changes.
- A source file is updated or restored.
Background checks cover only the top translation of each string. One request can contain translations from several saves.
The results from background checks are always shown with the Warning severity, even for a blocking check, because the translation is already saved.
If the module has supportsSynchronousRun: true, the app is also called while translations are being saved, before they are stored. This happens when translations are added:
- In the Editor, including Find and Replace and bulk saving.
- Using the API (Add Translation, Translation Batch Operations).
- By uploading files with translations.
- By auto-translation via MT, TM, or AI, including auto-translation in workflow steps.
- By vendor orders.
A synchronous check is called for every translation that is being saved, not only for the top translation. Other operations, such as approvals or translations copied between projects by a workflow, don’t call the check during the operation. They are checked in the background instead and can’t be blocked.
After a synchronous call, the background check still runs, but it calls the app only for checks that didn’t answer during the save. If the app answered during the save, it isn’t called a second time for the same translations.
When a translation fails a check with the Error severity, the app’s error.message is shown, and the translation isn’t saved:
| Where the translation is added | What happens |
|---|---|
| Editor, single translation | The translation isn’t saved. There is no Save Anyway option. |
| API, Add Translation | The API responds with HTTP 400 and the app’s message. |
| API, Translation Batch Operations | The whole request is rejected, and nothing is saved. |
| Editor, Find and Replace or bulk saving | Only the translations that failed the check aren’t saved. |
| File upload, auto-translation | The translations that failed the check are skipped. The report shows them as skipped by the QA check. |
To re-check existing translations with your app’s check, call the Revalidate QA Checks API method and pass the check ID in externalQaCheckIds. You can get the ID using the List External QA Checks API method. The Revalidate button in the project QA settings re-runs only Consistent terminology and AI-powered check.
Only one revalidation can run in a project at a time. Revalidation checks top translations only and doesn’t retry failed requests.
The Validate text by QA Checks API method checks translations without saving them. It calls all enabled external checks, synchronous or not, with the synchronous timeouts. If a check doesn’t answer, the response contains the Unable to complete check {name}. issue.
Crowdin Enterprise groups the translations of one operation by target language and file, and splits each group into batches of the size the app returns from getBatchSizeUrl.
- The default batch size is 500. It is also used if
getBatchSizeUrlisn’t set, if the request to it fails, or if it returns0or less. - The batch size is cached, so a new value takes effect within 10 minutes.
- The request to
getBatchSizeUrlhas a short time limit (2 seconds during a save and 10 seconds in the background). If the app doesn’t answer in time, the default of 500 is used. Return the batch size from a static value instead of computing it. - Up to 10 requests are sent at the same time for one set of translations. Different checks and languages of the same set are sent in parallel. Large operations, such as file uploads and auto-translation, are processed in chunks, one chunk after another.
- Many operations can run at the same time in an organization (Editor saves, file uploads, auto-translations, background checks). Each of them sends its own requests.
For example, when a file with 2,000 strings is uploaded with translations into 3 languages and the batch size is 500, the app receives 12 requests.
The time limit applies to each request, not to the whole operation.
| Call | Time limit for one request |
|---|---|
| Synchronous check, one translation | 10 seconds |
| Synchronous check, several translations | 60 seconds |
| Validate text by QA Checks API method | 10 seconds for one translation, 60 seconds for several translations |
| Background check and revalidation | 15 minutes |
Failed requests aren’t retried right away. Retries happen only for background checks, as described below.
A request fails when the app times out, the connection fails, the app responds with an HTTP error status, or the response is invalid. Several requests of one check are often sent together (for example, for one file upload). If one of them fails, the results of the other requests sent with it are also discarded, and all their translations are treated as not checked.
What happens next depends on how the check was called:
| Call | What happens on failure |
|---|---|
| Synchronous check | The translation is saved with the QA issue Unable to complete check {name}. (severity Warning). The background check calls the app again later. |
| Background check | The check is retried later, a limited number of times. If the last attempt also fails, the translations get the same issue. |
| Revalidation | The translations get the same issue right away, without retries. |
| Validate text by QA Checks API method | The response contains the same issue. Nothing is saved. |
The Unable to complete check {name}. issue never blocks a translation. It counts as a QA issue in filters and statistics, and it stays until the next successful check of that translation (for example, after the next save, a source text change, or revalidation).
To protect an app that is down, Crowdin Enterprise stops calling it for a short time after repeated failures:
- After repeated failed requests, calls to the check are paused for about a minute.
- The pause applies to the whole organization: all projects and all types of calls.
- While calls are paused, synchronous checks get
Unable to complete check {name}.right away, so blocking checks don’t block translations during the pause. Background checks wait for their next retry. - Timeouts, connection errors, HTTP
5xxand429responses, and invalid responses count as failures. Other HTTP4xxresponses don’t count, because they usually mean a problem with the request rather than an unavailable app. - The
Retry-Afterheader is ignored.
The app returns the result only in the HTTP response to the same request. If the check needs more time, choose one of these options:
- Keep the request open (background calls only). Background checks wait up to 15 minutes for one request. Use this for occasional long checks, not as the normal response time.
- Return an error and let Crowdin retry. An HTTP
5xxor429response makes the background check retry later, a limited number of times. Repeated errors pause the check for the whole organization, as described in Circuit Breaker. - Answer now and request a revalidation later. Respond right away, finish the work in the background, and then call the Revalidate QA Checks API method with your check ID in
externalQaCheckIds. This requires project manager permissions and re-checks the check for the whole project, so use it for rare, large updates.
If a synchronous check doesn’t answer within the time limit, the translation is saved, and the background check asks again with the 15-minute limit. So a synchronous check that is sometimes slow still gets its result, but later and without blocking.
Crowdin Enterprise doesn’t limit how often it calls an app. The only limits are the 10 parallel requests for one set of translations and the circuit breaker. Plan the capacity of your app, and of any service behind it, for these sources of load:
- Everyday work: Each save in the Editor or via the API sends a request. File uploads and auto-translations send one request per batch.
- Repeated texts: Results aren’t cached, and identical texts in different projects are checked separately. The same text is sent again after Save Anyway, an approval, a vote, a source text change, a retry, or a revalidation. Auto-translation via AI with retries on QA issues sends each text twice: once for the first AI output and once for the final text.
- Full re-checks: When a project manager enables the check in a project, or when the Revalidate QA Checks API method is called with its ID, the top translation of every string in the project is checked, in all languages. Your app doesn’t control when this happens.
- Parallel work: Operations in all projects of the organization run at the same time, and each of them sends its own requests.
For example, a project with 10,000 strings and 5 target languages has 50,000 top translations. Enabling the check in this project sends all of them, which is 100 requests with the default batch size.
If your app depends on a service with strict rate limits, follow these recommendations:
- Rate limiting: Put a queue and a rate limiter in front of the service. Crowdin Enterprise doesn’t reduce the request rate when the service is busy.
- Caching: Cache results by source text, translation text, target language, and the version of your check.
- Duplicates: Remove duplicates in a batch. Plural forms and repeated strings often share the same text.
- Batch calls: If the service supports it, send the whole batch to the service in one call instead of one call per translation.
- Batch size: Use a large batch size. Fewer, larger requests are easier to rate-limit. A batch size of up to 500 works well.
- Response time: Synchronous checks must answer well within the time limits.
- Busy responses: Don’t use HTTP
4xxto say “busy”. Background checks still retry these requests, and the circuit breaker doesn’t protect your service from them. - Unchecked translations: Don’t return
"passed": truewhen the check didn’t run. Translators won’t know that the translation wasn’t checked. - Consistent results: Return the same result for the same text. A result stays until the next check of the translation, so different results for the same text confuse translators.
- Re-check peaks: Size the app and the service for a full re-check of a large project, and let the rate limiter spread it over time.
- Synchronous runs: Set
supportsSynchronousRun: trueonly if the check answers reliably within the time limit. Otherwise, translators getUnable to complete check {name}.issues, and blocking doesn’t work when it’s needed.
| Problem | Likely cause | How to fix |
|---|---|---|
Translations show Unable to complete check {name}. | The app timed out, returned an HTTP error, or returned an invalid response. | Check that the response matches the format rules exactly and that the app answers within the time limits. |
| The check fails in all projects for about a minute | The circuit breaker paused the check after repeated failures. | Fix the cause of the failures. Calls resume automatically after about a minute. |
| The Error severity can’t be selected | The module doesn’t have supportsSynchronousRun: true. | Add "supportsSynchronousRun": true to the module in the manifest and update the app. |
| A blocking check doesn’t block some translations | The translation was added by an operation that doesn’t call synchronous checks, or the app didn’t answer in time. | See Synchronous Checks and Error Handling. |
| Results are mixed up between translations | The app stores or caches results by translationId. | Return translationId exactly as received and cache by text instead. |