│ │ │ or ...?state=&error=... │ │ │<──────────────────────────────────────────────────────────────────────┤ │ │ │ │ 7. GET /callback?state=...&code=... │ ├───────────────────────────────────>│ │ │ │ │ │ │ POST /api/auth/verify │ │ │ { │ │ │ "code": "", │ │ │ "userId": ..., │ │ │ "organizationId": ..., │ │ │ "ipAddress": ..., │ │ │ "moduleKey": "..." │ │ │ } │ │ ├─────────────────────────────────>│ │ │ │ │ │ { "success": true } │ │ │<─────────────────────────────────┤ │ │ │ │ 8. Access granted │ │ │<───────────────────────────────────┤ │ │ │ │ ``` #### [Initial Request to the App](#initial-request-to-the-app) [Section titled “Initial Request to the App”](#initial-request-to-the-app) Crowdin first attempts a direct check to see if access can be granted automatically. **HTTP request:** ```shell POST {AppBaseUrl}/api/auth/verify ``` **Request Headers** The request to your app will contain the following headers: * `Authorization: Bearer ` * `Content-Type: application/json` **Request Payload Example:** ```json { "userId": 12345, "organizationId": 67890, "ipAddress": "192.168.1.1", "moduleKey": "your-module-key" } ``` **Expected Response (Trigger Redirect):** To trigger the redirect flow, your app must return `success: false`. ```json { "success": false } ``` #### [Redirect and Callback](#redirect-and-callback) [Section titled “Redirect and Callback”](#redirect-and-callback) If the initial check returns `false`, Crowdin redirects the user to the `url` defined in your manifest options. **Redirect URL Structure:** ```shell https://{your-app-url}?jwtToken=&state= ``` Your app must validate the `jwtToken`, display the verification UI, and upon success, redirect the user back to Crowdin using an HTTP 302 redirect. **On Success:** ```http HTTP 302 Redirect Location: https://accounts.crowdin.com/{domain}/guard/callback?state=&code= ``` **On Failure:** ```http HTTP 302 Redirect Location: https://accounts.crowdin.com/{domain}/guard/callback?state=&error=User+denied+access ``` #### [Code Verification Request](#code-verification-request) [Section titled “Code Verification Request”](#code-verification-request) Once Crowdin receives the `code` from the callback, it sends a final request to your app to verify it. **HTTP request:** ```shell POST {AppBaseUrl}/api/auth/verify ``` **Request Headers** * `Authorization: Bearer ` * `Content-Type: application/json` **Request Payload Example:** ```json { "code": "abc123xyz", "userId": 12345, "organizationId": 67890, "ipAddress": "192.168.1.1", "moduleKey": "your-module-key" } ``` **Expected Response:** ```json { "success": true } ``` ### [Frame Type (Embedded UI)](#frame-type-embedded-ui) [Section titled “Frame Type (Embedded UI)”](#frame-type-embedded-ui) The Frame type displays your verification page within an iframe inside the Crowdin login interface. This provides a seamless user experience for custom forms or mTLS checks while keeping the user within the Crowdin environment. #### [Communication Flow](#communication-flow-2) [Section titled “Communication Flow”](#communication-flow-2) ```text ┌─────────┐ ┌──────────────┐ ┌──────────┐ │ User │ │ Crowdin │ │ Your App │ └────┬────┘ └──────┬───────┘ └────┬─────┘ │ │ │ │ 1. Login attempt │ │ ├───────────────────────────────>│ │ │ │ │ │ │ Try POST /api/auth/verify │ │ ├─────────────────────────────────>│ │ │ │ │ │ { "success": false } │ │ │<─────────────────────────────────┤ │ │ │ │ 2. Show verification page │ │ │ with embedded iframe │ │ │<───────────────────────────────┤ │ │ │ │ │ 3. Iframe loads your URL │ │ │ GET /frame?jwtToken=... │ │ ├──────────────────────────────────────────────────────────────────>│ │ │ │ │ 4. Your verification UI │ │ │<──────────────────────────────────────────────────────────────────┤ │ │ │ │ 5. User interacts with iframe │ │ │ (clicks approve/deny) │ │ │────────────────────────────────────────────────────────────────┐ │ │ │ │ │ │ 6. JavaScript API call via │ │ │ │ Crowdin Apps SDK │ │ │ │ AP.verifyAuth({ code: "..."}) │ │ ├────────────────────────────────────────────────────────────────┼─>│ │ │ │ │ │ 7. Crowdin receives code │<──────────────────────────────┘ │ │ from iframe via postMessage│ │ │ │ │ │ │ POST /api/auth/verify │ │ │ { │ │ │ "code": "...", │ │ │ "userId": ..., │ │ │ "moduleKey": "..." │ │ │ } │ │ ├─────────────────────────────────>│ │ │ │ │ │ { "success": true } │ │ │<─────────────────────────────────┤ │ │ │ │ 8. Access granted │ │ │<───────────────────────────────┤ │ │ │ │ ``` #### [Initial Request to the App](#initial-request-to-the-app-1) [Section titled “Initial Request to the App”](#initial-request-to-the-app-1) Crowdin first attempts a direct check to see if access can be granted automatically. **HTTP request:** ```shell POST {AppBaseUrl}/api/auth/verify ``` **Request Payload Example:** ```json { "userId": 12345, "organizationId": 67890, "ipAddress": "192.168.1.1", "moduleKey": "your-module-key" } ``` **Expected Response (Trigger Iframe):** To trigger the iframe flow, your app must return `success: false`. ```json { "success": false } ``` #### [Iframe Display and UI](#iframe-display-and-ui) [Section titled “Iframe Display and UI”](#iframe-display-and-ui) If the initial check returns `false`, Crowdin loads your app’s `url` in an iframe with the following parameters: ```shell https://{your-app-url}?jwtToken= ``` **Verification UI Implementation:** Create an HTML page that initializes the Crowdin Apps SDK and handles the user interaction. ```html Verification Security Verification
Please confirm your identity
``` #### [Communication via SDK](#communication-via-sdk) [Section titled “Communication via SDK”](#communication-via-sdk) Use the `AP.verifyAuth()` method to communicate the result back to Crowdin. **Success:** ```javascript AP.verifyAuth({ code: "your-verification-code" }); ``` **Failure:** ```javascript AP.verifyAuth({ error: "Verification failed: device not trusted" }); ``` #### [Code Verification Request](#code-verification-request-1) [Section titled “Code Verification Request”](#code-verification-request-1) Once Crowdin receives the `code` from the SDK, it sends a final request to your app to verify it. **HTTP request:** ```shell POST {AppBaseUrl}/api/auth/verify ``` **Request Payload Example:** ```json { "code": "abc123xyz", "userId": 12345, "organizationId": 67890, "ipAddress": "192.168.1.1", "moduleKey": "your-module-key" } ``` **Expected Response:** ```json { "success": true } ```
# Context Menu Module
> Create the item in the context menu where possible
This module allows creating custom items in Crowdin’s context menus. Crowdin context menus: * Resources > TM > TM record * Resources > Glossary > Glossary record * Project home tab > Language record * Project > Content > Files > File record * Project > Content > Screenshots > Screenshot record * Project > Language page > File record Crowdin Enterprise context menus: * Workspace > TM > TM record * Workspace > Glossary > Glossary record * Project home page > Language record * Project > Content > Files > File record * Project > Content > Screenshots > Screenshot record * Project home page > Language page > File record A context menu item can open a specified app module with additional context related to the selected record or custom URL. There are the following types of actions: * Open a specified app module in a modal dialog (see [Modal module](#modal)) * Redirect to a specified app module * Open a custom URL in a new tab ## [Access](#access) [Section titled “Access”](#access) You can grant access to this module to one of the following user categories: For Crowdin: * Only me (i.e., project owner) * Me, project managers and developers (if location: `language`, `screenshot`, `source_file`, `translated_file`) * All project members * Selected users For Crowdin Enterprise: * Only organization admins * Organization admins, project managers and developers (if location: `language`, `screenshot`, `source_file`, `translated_file`) * All users in the organization projects * Selected users ## [Structure](#structure) [Section titled “Structure”](#structure) ### [Modal](#modal) [Section titled “Modal”](#modal) Context menu item shows the specified module in the modal. manifest.json ```json { "modules": { "context-menu": [ { "key": "context-menu-key", "name": "Name of Context Menu Item", "description": "Description of Context Menu Item", "options": { "location": "source_file", "type": "modal", "module": { "project-integrations": "integration-module-key" }, "signaturePatterns": { "fileName": ".*\\.json", "nodeType": [0, 1] } } } ], "project-integrations": [ { "key": "integration-module-key", "name": "New Integration", "logo": "/integration-logo.png", "url": "/path/to/integration/module" } ] } } ``` ### [Redirect](#redirect) [Section titled “Redirect”](#redirect) Context menu item redirects to the specified module. manifest.json ```json { "modules": { "context-menu": [ { "key": "context-menu-key", "name": "Name of Context Menu Item", "description": "Description of Context Menu Item", "options": { "location": "source_file", "type": "redirect", "module": { "project-integrations": "integration-module-key" }, "signaturePatterns": { "fileName": ".*\\.json", "nodeType": [0, 1] } } } ], "project-integrations": [ { "key": "integration-module-key", "name": "New Integration", "logo": "/integration-logo.png", "url": "/path/to/integration/module" } ] } } ``` ### [New Tab](#new-tab) [Section titled “New Tab”](#new-tab) Context menu item opens a new tab with the URL: `baseUrl/options.url`. manifest.json ```json { "baseUrl": "https://app.example.com", "modules": { "context-menu": [ { "key": "context-menu-key", "name": "Name of Context Menu Item", "description": "Description of Context Menu Item", "options": { "location": "source_file", "type": "new_tab", "url": "/example/path", "signaturePatterns": { "fileName": ".*\\.json", "nodeType": [0, 1] } } } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `name` | **Type:** `string`**Required:** yes**Description:** The human-readable name displayed in the context menu. | | `description` | **Type:** `string`**Description:** A human-readable description of what the module does. The description will be visible in the Crowdin UI. | | `options.location` | **Type:** `string`**Required:** yes**Allowed values:** `tm`, `glossary`, `language`, `screenshot`, `source_file`, `translated_file`**Description:** The location in UI where the context menu can be added. | | `options.type` | **Type:** `string`**Required:** yes**Allowed values:** `modal`, `new_tab`, `redirect`**Description:** The type of action this module will perform. | | `options.url` | **Type:** `string`**Description:** Relative URL.Use it only with `new_tab` type | | `options.module` | **Type:** `object`**Description:** Module definition.Use it only with `modal` and `redirect` types | | `signaturePatterns` | **Type:** `object`**Description:** Contains criteria used to detect the type of file or node, specifying when the context menu item is shown.Use it only when `options.location` is set to `source_file` | | `signaturePatterns.fileName` | **Type:** `string`**Description:** Contains `fileName` regular expressions used to detect file type. | | `signaturePatterns.nodeType` | **Type:** `array`**Allowed values:** `0` – folder, `1` – file, `2` – branch**Description:** Array of node types specifying when the context menu item appears. | | `environments` | **Type:** `string`**Allowed values:** `crowdin`, `crowdin-enterprise`**Description:** Set of environments where a module could be installed. This parameter is needed for cross-product applications. |
# Custom AI Module
> Connect AI providers not yet supported by Crowdin
This module helps you connect AI providers not yet supported by Crowdin. Once you create this kind of app, you’ll be able to pre-translate your content with the connected AI provider and use the AI provider as an assistant in the Editor. ## [Access](#access) [Section titled “Access”](#access) You can grant access to this module to one of the following user categories: For Crowdin: * Only me (i.e., project owner) * All project members * Selected users For Crowdin Enterprise: * Only organization admins * All users in the organization projects * Selected users ## [Structure](#structure) [Section titled “Structure”](#structure) manifest.json ```json { "modules": { "ai-provider": [ { "key": "custom-ai", "name": "Custom Open AI", "logo": "/logo.png", "url": "/ai-settings", "chatCompletionsUrl": "/chat/completions", "modelsUrl": "/models" } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `name` | **Type:** `string`**Required:** yes**Description:** The human-readable name of the module. | | `logo` | **Type:** `string`**Required:** yes**Description:** The relative URL to the custom AI’s logo that will be displayed in the Crowdin UI. The recommended resolution is 48x48 pixels. | | `url` | **Type:** `string`**Required:** no**Description:** The relative URL to the module settings page (e.g., AI provider credentials settings, etc.). | | `chatCompletionsUrl` | **Type:** `string`**Required:** yes**Description:** The relative URL used for fetching chat completions. | | `modelsUrl` | **Type:** `string`**Required:** yes**Description:** The relative URL used for retrieving the list of models supported by the AI provider. | | `environments` | **Type:** `string`**Allowed values:** `crowdin`, `crowdin-enterprise`**Description:** Set of environments where a module could be installed. This parameter is needed for cross-product applications. | | `restrictAiToSameApp` | **Type:** `boolean`**Description:** If your application implements both AI Provider and AI Prompt Provider modules and you need to restrict them to work only in pairs, you can use the `restrictAiToSameApp` configuration property. When this property is enabled, prompts that use the application’s AI Provider can be saved only if they use the same application’s AI Prompt Provider, and vice versa. | ## [Communication between Custom AI App and Crowdin](#communication-between-custom-ai-app-and-crowdin) [Section titled “Communication between Custom AI App and Crowdin”](#communication-between-custom-ai-app-and-crowdin) Crowdin sends prompts to the app using `chatCompletionsUrl`, and the app then processes these prompts and responds to Crowdin with a reply. For an improved experience when interacting with your Custom AI as an assistant in the Crowdin Editor, it is recommended to implement the `stream` parameter using [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format). When streaming is supported, AI-generated content can be delivered progressively, providing real-time feedback to users. This improves the workflow by offering immediate suggestions or corrections without waiting for a complete response, ensuring a smooth, interactive experience for tasks like confirming terminology, checking consistency, or refining translations. Crowdin includes the `stream` parameter set to `true` in requests to the app for `chatCompletions` and expects a response in [the corresponding format](#expected-response-from-the-app-streaming). When configuring a Custom AI provider in the **AI > Providers** page, Crowdin sends a request to the app using `modelsUrl`, specifically to display available models in the respective input field in the Custom AI provider settings page. You can select and save needed models, which then will be used for content pre-translation and communication with the assistant using `chatCompletionsUrl`. ## [Request to the App from Crowdin for сhatCompletions](#request-to-the-app-from-crowdin-for-сhatcompletions) [Section titled “Request to the App from Crowdin for сhatCompletions”](#request-to-the-app-from-crowdin-for-сhatcompletions) **HTTP request:** ```shell https://{AppBaseUrl}/chat/completions/ ``` **Request Headers** The request to `chatCompletionsUrl` will contain authorization headers (e.g., `Authorization: Bearer `). Request payload example: ```json { "model": "gpt-3.5-turbo", "stream": false, "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Prompt" }, { "type": "image", "mimeType": "image/png", "url": "https://picsum.photos/200/300" } ] }, { "role": "assistant", "content": "Reply" }, { "role": "user", "content": "New prompt" } ] } ``` ## [Expected Response from the App](#expected-response-from-the-app) [Section titled “Expected Response from the App”](#expected-response-from-the-app) Response payload example: ```json { "choices": [ { "message": { "role": "assistant", "content": "New reply" } } ] } ``` ## [Expected Response from the App (Streaming)](#expected-response-from-the-app-streaming) [Section titled “Expected Response from the App (Streaming)”](#expected-response-from-the-app-streaming) Response payload example: ```json data: {"choices":[{"delta":{"role":"assistant","content":"Your"}}]} data: {"choices":[{"delta":{"role":"assistant","content":" rephrased"}}]} .... data: {"choices":[{"delta":{"role":"assistant","content":" translation"}}]} [DONE] ``` ## [Response from the App to Crowdin for modelsUrl](#response-from-the-app-to-crowdin-for-modelsurl) [Section titled “Response from the App to Crowdin for modelsUrl”](#response-from-the-app-to-crowdin-for-modelsurl) Response payload example: ```json { "data": [ { "id": "gpt-3.5-turbo", "supportsJsonMode": true, "supportsFunctionCalling": true, "supportsVision": false, "contextWindowLimit": 16385, "outputLimit": 4096 }, { "id": "gpt-4-turbo", "supportsJsonMode": true, "supportsFunctionCalling": true, "supportsVision": true, "contextWindowLimit": 128000, "outputLimit": 4096 }, { "id": "gpt-5-power", "supportsJsonMode": true, "supportsFunctionCalling": true, "supportsVision": true, "contextWindowLimit": 1000000, "outputLimit": 8192 }, { "id": "gpt-6-devil", "supportsJsonMode": true, "supportsFunctionCalling": true, "supportsStreaming": true, "supportsVision": true, "contextWindowLimit": 1000000, "outputLimit": 8192 }, { "id": "gpt-7-magic", "supportsJsonMode": true, "supportsFunctionCalling": true, "supportsStreaming": true, "supportsVision": true, "contextWindowLimit": 1000000, "outputLimit": 8192 } ] } ``` Default values: * **supportsJsonMode**: `false` * **supportsFunctionCalling**: `false` * **supportsStreaming**: `false` * **supportsVision**: `false` * **contextWindowLimit**: `4096` * **outputLimit**: `4096` The structure of the responses from the app should correspond to the presented examples, otherwise Crowdin will consider them invalid.
# Custom Bundle Generator Module
> Add support for new custom bundles to Crowdin
Use this module to support custom formats of [Target File Bundles](/bundles/). The generation process is delegated to your Crowdin app that implements a custom bundle generator module. When translations are ready, Crowdin sends the translation data to your app, which returns the generated bundle in the desired format. ## [Access](#access) [Section titled “Access”](#access) You can grant access to this module to one of the following user categories: For Crowdin: * Only me (i.e., project owner) * All project members * Selected users For Crowdin Enterprise: * Only organization admins * All users in the organization projects * Selected users ## [Structure](#structure) [Section titled “Structure”](#structure) manifest.json ```json { "modules": { "custom-file-format": [ { "key": "your-module-key-type-xyz", "type": "type-xyz", "url": "/process", "multilingualExport": true, "stringsExport": true, "extensions": [ ".resx" ] } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `type` | **Type:** `string`**Required:** yes**Description:** Identifier of the custom bundle type. Can be used in the API to trigger the processing of this module when generating translation bundles. | | `url` | **Type:** `string`**Required:** yes**Description:** The relative URL triggered during translation export. Crowdin sends a request to this URL to initiate custom bundle generation. | | `multilingualExport` | **Type:** `bool`**Required:** no**Allowed values:** `true`, `false`. Default is `false`**Description:** Enables export of strings for multiple target languages in a single request. Useful for file formats that support multiple languages within the same bundle. | | `stringsExport` | **Type:** `bool`**Required:** yes**Description:** Must be set to `true` for bundle generator modules. Indicates that the app expects exported strings from Crowdin to generate a custom bundle. | | `extensions` | **Type:** `array`**Required:** yes**Description:** List of file extensions associated with the generated bundles (e.g., `.resx`, `.json`). Defines the expected output format and is used for file naming and export handling in Crowdin. | ## [Communication between Custom Bundle Generator App and Crowdin](#communication-between-custom-bundle-generator-app-and-crowdin) [Section titled “Communication between Custom Bundle Generator App and Crowdin”](#communication-between-custom-bundle-generator-app-and-crowdin) When a user requests a translation bundle export, Crowdin sends an HTTP request to the app’s configured URL (`$baseUrl . $url`) with the necessary project, language, and translation data. The app processes the data and responds with the generated bundle file. The requests and responses to and from the custom bundle generator apps have two-minute timeouts. The maximum request and response payload size is limited to 5 MB. ### [Request to the App](#request-to-the-app) [Section titled “Request to the App”](#request-to-the-app) Request payload example: ```json // max request payload - 5 MB // wait timeout - 2 minutes { "jobType": "build-file", "organization": { "id": 1, "domain": "{domain}", "baseUrl": "https://{domain}.crowdin.com", "apiBaseUrl": "https://{domain}.api.crowdin.com" }, "project": { "id": 1, "identifier": "your-project-identifier", "name": "Your Project Name" }, "sourceLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "pluralCategoryNames": ["one"], "pluralRules": "(n != 1)" }, "targetLanguages": [ { // same structure as for sourceLanguage } ], "strings": [...], // array of segments "stringsUrl": "https://tmp.downloads.crowdin.com/strings.ndjson" // file with segments, in new-line delimited json format } ``` Properties: | | | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `jobType` | **Type:** `string`**Possible values:** `build-file`**Description:** Specifies the action that should be executed by the app. Always set to `build-file` for bundle generator modules. Crowdin sends translation data and expects a generated bundle in return. | | `strings`, `stringsUrl` | **Type(strings):** `array`**Type(stringsUrl):** `string`**Description:** Contains the translation strings for the bundle generation. Either `strings` (inline array) or `stringsUrl` (public link to NDJSON) can be used. | ### [Expected Response from the App](#expected-response-from-the-app) [Section titled “Expected Response from the App”](#expected-response-from-the-app) Response payload example: ```json // max response payload - 5 MB // wait timeout - 2 minutes { "data": { "content": "TWF5IHRoZSBGb3JjZSBiZSB3aXRoIHlvdS4=", // base64 encoded translation file content "contentUrl": "https://app.example.com/p5uLEpq8p-result.xml", // translation file public URL }, "error": { "message": "Your error message" } } ``` Properties: | | | | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `data.content`, `data.contentUrl` | **Type(data.content):** `string`**Type(data.contentUrl):** `string`**Description:** Use either `data.content` (base64 encoded) or `data.contentUrl` (publicly accessible URL) to return the generated bundle file. Only one of them should be present in the response.The format of the file depends on your implementation. | | `error.message` | **Type:** `string`**Description:** An error message that can be passed from the app to Crowdin and will be visible to a user in the UI. | ### [Translation Strings Structure](#translation-strings-structure) [Section titled “Translation Strings Structure”](#translation-strings-structure) Below is an example of the structure used to pass translation strings to the app for custom bundle generation. Payload example: ```json // strings should be in "new-line delimited json" format if they passed by URL [ { // non plural string "id": 1, // numeric identifier of the string in Crowdin "identifier": "string-key-1", // required: unique string key "context": "Some context", // optional: additional info for translators "customData": "max 4 KB of custom data", // optional: preserved on export "maxLength": 10, // optional, default null "isHidden": false, // optional, default null "hasPlurals": false, // optional, default false "labels": ["label-one", "label-two"], // optional, default [] "text": "String source text", // required: source content "translations": { // required: grouped by target language ID "uk": { // targetLanguage.id "text": "Переклад стрічки", // required: translation text "status": "untranslated | translated | approved" // optional, default "translated" }, // can be other languages for multilingual, check "targetLanguages" in the request payload } }, { // plural string "id": 2, "identifier": "string-key-2", "context": "Some optional context", "customData": "max 4 KB of custom data", "maxLength": 15, "isHidden": false, "hasPlurals": true, "labels": [], "text": { // keys from sourceLanguage.pluralCategoryNames "one": "One file", "other": "%d files", }, "translations": { "uk": { "text": { // keys from targetLanguage.pluralCategoryNames "one": "One file", "few": "%d файла", "many": "%d файлів", }, "status": { "one": "untranslated", "few": "translated", "many": "approved", } } } } ] ``` Properties: | | | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | **Type:** `integer`**Required:** yes**Description:** Numeric identifier of the string in your Crowdin project. Required for mapping translations. | | `identifier` | **Type:** `string`**Required:** yes**Description:** Unique string key within the file. Required. | | `text` | **Type:** `string` (non-plural) or `object` (plural)**Description:** Source string text. Required for generating translations. For plural strings, this is an object with plural form keys from `sourceLanguage.pluralCategoryNames`. | | `customData` | **Type:** `string`**Required:** no**Description:** Any custom data that needs to be linked to the string. Added custom data will be exported along the corresponding strings on translation export. | | `translations` | **Type:** `object`**Required:** yes**Description:** Required translations for each target language. Each language ID maps to an object with a `text` field, and optionally `status`. For plural strings, `text` and `status` are also objects keyed by plural category names. |
# Custom File Format Module
> Add support of new custom file formats to Crowdin
Use this module to add support of new custom file formats. It’s implemented by delegating a source file parsing to an app with a custom file format module. When translations are completed, Crowdin passes a source file and a string array with translations to the Custom file format app for translation files generation. ## [Access](#access) [Section titled “Access”](#access) You can grant access to this module to one of the following user categories: For Crowdin: * Only me (i.e., project owner) * All project members * Selected users For Crowdin Enterprise: * Only organization admins * All users in the organization projects * Selected users ## [Structure](#structure) [Section titled “Structure”](#structure) manifest.json ```json { "modules": { "custom-file-format": [ { "key": "your-module-key-type-xyz", "type": "type-xyz", "url": "/process", "multilingual": true, "signaturePatterns": { "fileName": "^.+\\.xyz$", "fileContent": "\\s* " } } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `type` | **Type:** `string`**Required:** yes**Description:** The custom file format identifier. Can be used in API to force the processing of the files by the Custom file format app. If the `type` parameter is used in API, the `signaturePatterns` will be ignored. | | `url` | **Type:** `string`**Required:** yes**Description:** The relative URL triggered on file import, update, translation upload, and export. | | `multilingual` | **Type:** `bool`**Required:** no**Allowed values:** `true`, `false`. Default is `false`**Description:** This parameter is used to combine the content of multiple languages into one request when uploading and downloading translations in your Crowdin project. | | `signaturePatterns` | **Type:** `object`**Description:** Contains `fileName` and/or `fileContent` regular expressions used to detect file type when uploading a new source file via UI (or via API without specified `type` parameter). If the file matches regular expressions, it’s labeled as a custom format file. | ## [Communication between Custom File Format App and Crowdin](#communication-between-custom-file-format-app-and-crowdin) [Section titled “Communication between Custom File Format App and Crowdin”](#communication-between-custom-file-format-app-and-crowdin) On the initial file import, the system detects custom file format using the `signaturePatterns` or `type` parameters and makes an HTTP request to the app’s URL (`$baseUrl . $url`) for further processing. Then app processes the file in a custom format and responds to the system. The requests and responses to and from the custom file format apps have two-minute timeouts. The maximum request and response payload size is limited to 5 MB. ### [Request to the App](#request-to-the-app) [Section titled “Request to the App”](#request-to-the-app) Request payload example: ```json // max request payload - 5 MB // wait timeout - 2 minutes { "jobType": "parse-file | build-file", "organization": { "id": 1, "domain": "{domain}", "baseUrl": "https://{domain}.crowdin.com", "apiBaseUrl": "https://{domain}.api.crowdin.com" }, "project": { "id": 1, "identifier": "your-project-identifier", "name": "Your Project Name" }, "file": { "id": 1, "name": "file.xml", "content": "VGhpcyBpcyBmaWxlIGNvbnRlbnQ=", // base64 encoded source file content "contentUrl": "https://crowdin-tmp.downloads.crowdin.com/1/file.xml?aws-signature=..." // source file public URL }, "sourceLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "pluralCategoryNames": ["one"], "pluralRules": "(n != 1)" }, "targetLanguages": [ { // same structure as for sourceLanguage, empty when uploading a new source file, one element for import_translations & export, can be more for multilingual files } ], "strings": [...], // for the build-file jobs, array of segments "stringsUrl": "https://tmp.downloads.crowdin.com/strings.ndjson" // for the build-file jobs, file with segments, in new-line delimited json format } ``` Properties: | | | | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `jobType` | **Type:** `string`**Possible values:** `parse-file`, `build-file`**Description:** Specifies the action that should be executed by the app. `parse-file` job is used for initial source file upload, source file update, and translation upload. For `parse-file` jobs, the system passes a source file to the app and expects a parsed source string array in the response. `build-file` job is used for translation download. For `build-file` jobs, the system passes a source file and a string array with translations to the app and expects a generated translation file in the response. | | `file.content`, `file.contentUrl` | **Type:** `string`**Description:** Parameters used to pass the base64 encoded source file content (`file.content`) or a source file public URL (`file.contentUrl`). Either of these two parameters can be used. | | `strings`, `stringsUrl` | **Type(strings):** `array`**Type(stringsUrl):** `string`**Description:** Parameters used for translations download (for `build-file` job type only). `strings` - translation strings array. `stringsUrl` - public URL to a [new-line delimited json](https://github.com/ndjson/ndjson-spec) with translation strings. Either of these two parameters can be used. | ### [Expected Response from the App for the parse-file Job Type](#expected-response-from-the-app-for-the-parse-file-job-type) [Section titled “Expected Response from the App for the parse-file Job Type”](#expected-response-from-the-app-for-the-parse-file-job-type) Response payload example: ```json // max response payload - 5 MB // wait timeout - 2 minutes { "data": { "strings": [...], // segments array "stringsUrl": "https://app.example.com/jKe8ujs7a-segments.ndjson", // new-line delimited json file with parsed strings "preview": "VGhpbmdzIGFyZSBvbmx5IGltcG9zc2libGUgdW50aWwgdGhleSdyZSBub3Qu", // optional, base64 encoded content of preview html file, not supported if there are plural strings "previewUrl": "https://app.example.com/LN3km2K6M-preview.html", // optional, URL of preview html file, not supported if there are plural strings }, "error": { "message": "Your error message" } } ``` Properties: | | | | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `data.strings`, `data.stringsUrl` | **Type(data.strings):** `array`**Type(data.stringsUrl):** `string`**Description:** Parameters used to pass the parsed strings content. `data.strings` - parsed strings array. `data.stringsUrl` - public URL to a [new-line delimited json](https://github.com/ndjson/ndjson-spec) with parsed strings. Either of these two parameters can be used. | | `data.preview`, `data.previewUrl` | **Type(data.preview):** `string`**Type(data.previewUrl):** `string`**Description:** Parameters used to pass the optional HTML preview of the parsed strings content, which can be generated by the app. The generated HTML preview will be displayed in the Editor. See the [HTML Preview file example.](/developer/crowdin-apps-module-custom-file-format/#html-preview-of-the-file)CautionHTML preview won’t be displayed in the Crowdin Editor if the app passes strings with plurals. | | `error.message` | **Type:** `string`**Description:** An error message that can be passed from the app to Crowdin Enterprise and will be visible to a user in the UI. | ### [Expected Response from the App for the build-file Job Type](#expected-response-from-the-app-for-the-build-file-job-type) [Section titled “Expected Response from the App for the build-file Job Type”](#expected-response-from-the-app-for-the-build-file-job-type) Response payload example: ```json // max response payload - 5 MB // wait timeout - 2 minutes { "data": { "content": "TWF5IHRoZSBGb3JjZSBiZSB3aXRoIHlvdS4=", // base64 encoded translation file content "contentUrl": "https://app.example.com/p5uLEpq8p-result.xml", // translation file public URL }, "error": { "message": "Your error message" } } ``` Properties: | | | | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `data.content`, `data.contentUrl` | **Type(data.content):** `string`**Type(data.contentUrl):** `string`**Description:** Parameters used to pass the base64 encoded translation file content (`data.content`) or a translation file public URL (`data.contentUrl`). Either of these two parameters can be used. | | `error.message` | **Type:** `string`**Description:** An error message that can be passed from the app to Crowdin Enterprise and will be visible to a user in the UI. | ### [Strings Array Structure](#strings-array-structure) [Section titled “Strings Array Structure”](#strings-array-structure) Below you can see an example of the strings structure expected from the app for `parse-file` job type and passed to the app for `build-file` job type. Payload example: ```json // strings should be in "new-line delimited json" format if they passed by URL [ { // non plural string "previewId": 1, // only for "parse-file" jobType, required when the HTML preview of the file is generated "id": 1, // only for "build-file" jobType "identifier": "string-key-1", // required "context": "Some context", // optional "customData": "max 4 KB of custom data", // optional "maxLength": 10, // optional, default null "isHidden": false, // optional, default null "hasPlurals": false, // optional, default false "labels": ["label-one", "label-two"], // optional, default [] "text": "String source text", // required "translations": { // optional "uk": { // targetLanguage.id "text": "Переклад стрічки", // required "status": "untranslated | translated | approved" // optional, default "translated" }, // can be other languages for multilingual, check "targetLanguages" in the request payload } }, { // plural string "previewId": 2, "id": 2, "identifier": "string-key-2", "context": "Some optional context", "customData": "max 4 KB of custom data", "maxLength": 15, "isHidden": false, "hasPlurals": true, "labels": [], "text": { // keys from sourceLanguage.pluralCategoryNames "one": "One file", "other": "%d files", }, "translations": { "uk": { "text": { // keys from targetLanguage.pluralCategoryNames "one": "One file", "few": "%d файла", "many": "%d файлів", }, "status": { "one": "untranslated", "few": "translated", "many": "approved", } } } } ] ``` Properties: | | | | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `previewId` | **Type:** `integer`**Required:** yes (only for the `parse-file` job when the HTML preview of the file is generated)**Description:** Numeric identifier of the string in the HTML Preview file. Used for `parse-file` job type only. | | `id` | **Type:** `integer`**Description:** Numeric identifier of the string in your Crowdin Enterprise project. Used for `build-file` job type only. | | `identifier` | **Type:** `string`**Description:** Unique string key within the file. | | `customData` | **Type:** `string`**Description:** Any custom data that need to be linked to the string. Added custom data will be exported along the corresponding strings on translation export. | ## [HTML Preview of the File](#html-preview-of-the-file) [Section titled “HTML Preview of the File”](#html-preview-of-the-file) HTML Preview of the file example: ```html Optional Title HTML preview of the file
Key: Text: Key 1 Source Text 1 Key 2 Source Text 2
```
# Custom MT Module
> Connect custom machine translation engines to Crowdin
This module helps you connect machine translation engines not yet supported by Crowdin. Once you create this kind of app, you’ll be able to pre-translate your content with the connected MT or enable translation suggestions made by it to be shown in the Editor for translators.  ## [Access](#access) [Section titled “Access”](#access) You can grant access to this module to one of the following user categories: For Crowdin: * Only me (i.e., project owner) * All project members * Selected users For Crowdin Enterprise: * Only organization admins * All users in the organization projects * Selected users ## [Structure](#structure) [Section titled “Structure”](#structure) manifest.json ```json { "modules": { "custom-mt": [ { "key": "custom-mt", "name": "Custom MT", "logo": "/logo.png", "url": "/translate", "withContext": true, "batchSize": 10, "splitStringsIntoChunks": true } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `name` | **Type:** `string`**Required:** yes**Description:** The human-readable name of the module. | | `logo` | **Type:** `string`**Required:** yes**Description:** The relative URL to the custom MT’s logo that will be displayed in the Crowdin UI. The recommended resolution is 48x48 pixels. | | `url` | **Type:** `string`**Required:** yes**Description:** The relative URL to the content page of the module that will be integrated with Crowdin. | | `withContext` | **Type:** `boolean`**Required:** no**Description:** The additional meta data that will be sent along the strings. | | `batchSize` | **Type:** `integer`**Required:** no**Description:** The maximum quantity of strings that can be sent to the Custom MT app in one request. | | `splitStringsIntoChunks` | **Type:** `boolean`**Required:** no**Default:** `true`**Description:** Controls [batch processing](#batch-processing-and-file-context). If set to `false`, all strings from a file are sent to the MT engine in a single request. If set to `true` (default), Crowdin splits the strings into chunks. | | `environments` | **Type:** `string`**Allowed values:** `crowdin`, `crowdin-enterprise`**Description:** Set of environments where a module could be installed. This parameter is needed for cross-product applications. | ## [Communication between Custom MT App and Crowdin](#communication-between-custom-mt-app-and-crowdin) [Section titled “Communication between Custom MT App and Crowdin”](#communication-between-custom-mt-app-and-crowdin) The system sends texts for translation using `url` and then the app processes the texts and responds back to the system with one of the two possible types of responses: [without errors](#expected-response-from-the-app-without-errors), and [with errors](#expected-response-from-the-app-with-errors). **HTTP request:** ```shell https://{AppBaseUrl}/translate/?source=en&target=uk&project_id=727186&jwtToken={yourTokenValue} ``` ## [Query parameters](#query-parameters) [Section titled “Query parameters”](#query-parameters) | | | | ------------ | -------------------------------------------------------------------------------- | | `source` | **Type:** `string`**Description:** Source language. | | `target` | **Type:** `string`**Description:** Target language. | | `project_id` | **Type:** `integer`**Description:** The numeric identifier of a Crowdin project. | | `jwtToken` | **Type:** `string`**Description:** JWT token used for authorization. | | `strings` | **Type:** `string`**Description:** Source strings that require translation. | ## [Batch Processing and File Context](#batch-processing-and-file-context) [Section titled “Batch Processing and File Context”](#batch-processing-and-file-context) By default, Crowdin splits source strings into smaller chunks (`splitStringsIntoChunks: true`) to optimize processing time and manage request sizes. However, some modern MT engines and AI providers (such as XL8 or LLMs) require the full context of a file to produce high-quality translations. This is particularly important for formats like subtitles (SRT) or literary content where the translation of one sentence depends heavily on the previous one. If you are connecting an engine that supports file-based or document-level translation: 1. Set `splitStringsIntoChunks` to `false` in your manifest. 2. Crowdin will send all strings belonging to a single file in one request. Caution When `splitStringsIntoChunks` is set to `false`, ensure your endpoint and the connected MT engine can handle large payloads, as the request body size will correspond to the size of the full source file. ## [Handling Non-Translatable Elements by Your MT Engine](#handling-non-translatable-elements-by-your-mt-engine) [Section titled “Handling Non-Translatable Elements by Your MT Engine”](#handling-non-translatable-elements-by-your-mt-engine) For strings containing non-translatable elements (e.g., tags, placeholders, etc.), Crowdin replaces these elements with special *notranslate* tags. This ensures that these elements remain in their original state after the string is translated by the MT engine. Crowdin uses this approach to avoid potential issues that could break exported translation files. Below you can see the examples of a string before and after the modification. Here is an example of how a string containing non-translatable elements (tags, placeholders, etc.) looks in Crowdin: ```html Task: ``` This is how Crowdin modifies the above string before sending it to the MT engine: ```html 0Task:1 ``` ### [Customizing Non-Translatable Elements for Your MT Engine](#customizing-non-translatable-elements-for-your-mt-engine) [Section titled “Customizing Non-Translatable Elements for Your MT Engine”](#customizing-non-translatable-elements-for-your-mt-engine) If your MT engine already has a similar feature but implements it differently from Crowdin, we recommend adjusting the handling of non-translatable elements in your Custom MT app to match your MT engine’s implementation. Specifically, replace Crowdin’s defaults, like ```html %index% ``` with do-not-translate elements specific to your MT engine. Here you can explore an implementation example of do-not-translate elements in Amazon Translate: [Using do-not-translate in Amazon Translate](https://docs.aws.amazon.com/translate/latest/dg/customizing-translations-tags.html). ### [Handling Translations with Altered Non-Translatable Elements](#handling-translations-with-altered-non-translatable-elements) [Section titled “Handling Translations with Altered Non-Translatable Elements”](#handling-translations-with-altered-non-translatable-elements) If the MT engine sends a translation to Crowdin that doesn’t include all tags in their original state or if they are somehow altered (e.g., translated), Crowdin will ignore such translations and won’t save them to the string. ## [Request to the App from Crowdin for applicationUrl (simple)](#request-to-the-app-from-crowdin-for-applicationurl-simple) [Section titled “Request to the App from Crowdin for applicationUrl (simple)”](#request-to-the-app-from-crowdin-for-applicationurl-simple) Request payload example: ```json { "strings": [ "Save as...", "New file", "You received one message.", "You received {number} messages." ] } ``` ## [Request to the App from Crowdin for applicationUrl (extended)](#request-to-the-app-from-crowdin-for-applicationurl-extended) [Section titled “Request to the App from Crowdin for applicationUrl (extended)”](#request-to-the-app-from-crowdin-for-applicationurl-extended) To use the extended request please add the `withContext` parameter to your Custom MT module. Request payload example: ```json { "strings": [ { "id": 1, "projectId": 727186, "fileId": 47047, "text": "Save as...", "identifier": "save_as", "context": "translation Context", "maxLength": 15, "isHidden": false, "isPlural": false, "pluralForm": null }, ] } ``` ## [Expected Response from the App (Without errors)](#expected-response-from-the-app-without-errors) [Section titled “Expected Response from the App (Without errors)”](#expected-response-from-the-app-without-errors) Response payload example: ```json { "data": { "translations": [ "Зберегти як...", "Новий файл", "Ви отримали одне нове повідомлення.", "Ви отримали {number} нових повідомлень." ] } } ``` ## [Expected Response from the App (With errors)](#expected-response-from-the-app-with-errors) [Section titled “Expected Response from the App (With errors)”](#expected-response-from-the-app-with-errors) Response payload example: ```json { "error": { "message": "Error message from the App or MT engine" } } ``` The structure of the responses from the app should correspond to the presented examples, otherwise Crowdin will consider them invalid.
# Custom Spellchecker Module
> Add a custom spellchecker to verify translations against specific rules
This module allows you to add custom spellcheckers to verify translations against specific rules that are not supported by default. Each language can be assigned to only one active spellchecker. When using multiple custom spellcheckers, languages selected for a specific spellchecker are automatically unassigned from all other spellcheckers. Languages not assigned to any of the custom spellcheckers will be processed by the default Crowdin spellchecker. ## [Access](#access) [Section titled “Access”](#access) 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 ## [Structure](#structure) [Section titled “Structure”](#structure) manifest.json ```json { "modules": { "custom-spellchecker": [ { "key": "custom-spellchecker", "name": "Custom Spellchecker", "description": "Description", "checkSpellingUrl": "/check", "listSupportedLanguagesUrl": "/languages", "url": "/setup.html" } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `name` | **Type:** `string`**Required:** yes**Description:** The human-readable name of the module. | | `description` | **Type:** `string`**Description:** The human-readable description of what the module does. The description will be visible in the Crowdin Enterprise UI. | | `checkSpellingUrl` | **Type:** `string`**Required:** yes**Description:** The relative URL triggered when sending texts for spell checking. | | `listSupportedLanguagesUrl` | **Type:** `string`**Required:** yes**Description:** The relative URL triggered when retrieving the list of languages supported by the module. | | `url` | **Type:** `string`**Required:** yes**Description:** The relative URL to the module setup page. | | `environments` | **Type:** `string`**Allowed values:** `crowdin-enterprise`**Description:** Set of environments where a module could be installed. This parameter is needed for cross-product applications. | ## [Communication between Custom Spellchecker App and Crowdin](#communication-between-custom-spellchecker-app-and-crowdin) [Section titled “Communication between Custom Spellchecker App and Crowdin”](#communication-between-custom-spellchecker-app-and-crowdin) The system sends texts for spell checking using `checkSpellingUrl` either from the Editor during translation or when the QA Checks validate translations. The app then processes the texts and responds back to the system with one of the two possible types of responses: [without spelling issues](#expected-response-from-the-app-without-spelling-issues), and [with spelling issues](#expected-response-from-the-app-with-spelling-issues). There are cases when the system needs to check the languages supported by the module (e.g., when configuring languages for Custom Spellchecker in the Organization Settings). In these cases, Crowdin sends a request to the app using `listSupportedLanguagesUrl` and the app responds with the data about the languages it supports. ## [Request to the App from Crowdin for checkSpellingUrl](#request-to-the-app-from-crowdin-for-checkspellingurl) [Section titled “Request to the App from Crowdin for checkSpellingUrl”](#request-to-the-app-from-crowdin-for-checkspellingurl) Request payload example: ```json { "language": "uk", "texts": [ "Збререгти якк...", "Ноаий файд" ] } ``` ## [Expected Response from the App (Without spelling issues)](#expected-response-from-the-app-without-spelling-issues) [Section titled “Expected Response from the App (Without spelling issues)”](#expected-response-from-the-app-without-spelling-issues) Response payload example: ```json { "data": [ { "text": "Зберегти як...", "matches": [] }, { "text": "Новий файл", "matches": [] } ] } ``` ## [Expected Response from the App (With spelling issues)](#expected-response-from-the-app-with-spelling-issues) [Section titled “Expected Response from the App (With spelling issues)”](#expected-response-from-the-app-with-spelling-issues) Response payload example: ```json { "data": [ { "text": "Збререгти якк...", "matches": [ { "category": "typos", "message": "Знайдено потенційну орфографічну помилку.", "shortMessage": "Орфографічна помилка", "offset": 0, "length": 9, "replacements": [ "Зберегти" ] }, { "category": "typos", "message": "Знайдено потенційну орфографічну помилку.", "shortMessage": "Орфографічна помилка", "offset": 10, "length": 3, "replacements": [ "як" ] } ] }, { "text": "Ноаий файд", "matches": [ ] } ] } ``` **Expected category types:** * `typography` * `casing` * `grammar` * `typos` * `spelling` * `punctuation` * `confused_words` * `redundancy` * `style` * `gender_neutrality` * `semantics` * `colloquialisms` * `wikipedia` * `barbarism` * `misc` ## [Response from the App to Crowdin for listSupportedLanguagesUrl](#response-from-the-app-to-crowdin-for-listsupportedlanguagesurl) [Section titled “Response from the App to Crowdin for listSupportedLanguagesUrl”](#response-from-the-app-to-crowdin-for-listsupportedlanguagesurl) Response payload example: ```json { "data": [ { "code": "uk", "name": "Ukrainian" }, { "code": "kab", "name": "Kabul" }, { "code": "en", "name": "English" } ] } ``` The structure of the responses from the app should correspond to the presented examples, otherwise Crowdin will consider them invalid. Languages that are not available in the organization won’t be displayed in the module’s language list. To display such languages, add them as custom languages. Read more about [Custom Languages](/enterprise/organization-settings/#custom-languages).
# Editor Asset Panel Module
> Replace the default asset preview in the Editor with a custom app for specific file types
The Editor Asset Panel module allows your app to replace the default asset preview panel in the Editor. When a user opens an asset, your app can take over the entire preview area if the asset’s file name matches a pattern you define. This functionality is particularly useful for handling file types that are not natively previewable in the Editor, such as video, audio, or other proprietary formats. If an asset’s file name matches the `fileNamePattern` specified in your app’s manifest, your app will be loaded in place of the default preview. If there is no match, the standard Crowdin preview panel will be displayed.  ## [Access](#access) [Section titled “Access”](#access) You can grant access to this module to one of the following user categories: For Crowdin: * Only me (i.e., project owner) * Me, project managers and developers * All project members * Selected users For Crowdin Enterprise: * Only organization admins * Organization admins, project managers and developers * All users in the organization projects * Selected users ## [Structure](#structure) [Section titled “Structure”](#structure) manifest.json ```json { "modules": { "editor-asset-panel": [ { "key": "your-module-key", "name": "Module name", "url": "/editor-page", "fileNamePattern": "^.+\\.xyz$", "environments": [ "crowdin", "crowdin-enterprise" ] } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `name` | **Type:** `string`**Required:** yes**Description:** The human-readable name of the module. | | `url` | **Type:** `string`**Required:** yes**Description:** The relative URL to the module content page that will be embedded in the Editor’s asset panel. | | `fileNamePattern` | **Type:** `string`**Required:** yes**Description:** A regular expression that Crowdin uses to match the file names of assets your app can handle. If the asset’s name matches this pattern, your app will be loaded in the preview panel. For example, to match all MOV files, use `”^.+\.mov$“`. | | `environments` | **Type:** `string`**Allowed values:** `crowdin`, `crowdin-enterprise`**Description:** Set of environments where a module could be installed. This parameter is needed for cross-product applications. | ## [How It Works](#how-it-works) [Section titled “How It Works”](#how-it-works) The Editor Asset Panel module operates on an event-driven mechanism. When a user navigates to an asset within the Editor, the system checks for any installed apps that use this module. 1. **Pattern Matching**: Crowdin takes the `fileNamePattern` from your app’s `manifest.json` and tests it against the name of the currently opened asset (e.g., `clip.mov`). 2. **App Initialization**: If the asset’s file name matches the pattern, Crowdin replaces the default preview panel with an iframe pointing to your app’s `url`. 3. **Event Handling**: Once your app is loaded, Crowdin dispatches events to it, which your app must subscribe to and handle. These events contain the necessary data (payload) for your app to display the correct asset content. The primary events are: * `asset.source.preview`: Dispatched when the user clicks on the source asset preview. * `asset.suggestion.preview`: Dispatched when the user clicks on a translated asset preview. Your app should listen for these events to implement its custom preview logic, such as rendering a video player or an audio console. If an asset’s file name does not match the `fileNamePattern`, the default Crowdin asset previewer will be loaded as usual.
# Editor Right Panel Module
> Create additional tabs in the Editor's right panel
The module allows the creation of additional tabs in the Editor’s right panel. When using this module in your Crowdin app, you can choose the Editor mode where you’d like the additional tabs to be displayed.  ## [Access](#access) [Section titled “Access”](#access) You can grant access to this module to one of the following user categories: For Crowdin: * Only me (i.e., project owner) * Me, project managers and developers * All project members * Selected users For Crowdin Enterprise: * Only organization admins * Organization admins, project managers and developers * All users in the organization projects * Selected users ## [Structure](#structure) [Section titled “Structure”](#structure) manifest.json ```json { "modules": { "editor-right-panel": [ { "key": "your-module-key", "name": "Module name", "modes": [ "translate" ], "supportsMultipleStrings": false, "url": "/editor-page" } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `name` | **Type:** `string`**Required:** yes**Description:** The human-readable name of the module. | | `modes` | **Type:** `array`**Required:** yes**Allowed values:** `translate`, `comfortable`, `side-by-side`, `multilingual`, `review`, `assets`**Description:** The Editor’s mode list where the module will be available. Use `translate` to make the module available in the following views: `comfortable`, `side-by-side`, and `multilingual`. For more granular control, specify one or more of these values directly. The `review` mode is available only in Crowdin Enterprise and only on the [Source Text Review](/enterprise/source-text-review/) workflow step. The `assets` mode is used for managing assets in the Editor. | | `supportsMultipleStrings` | **Type:** `boolean`**Description:** Indicates whether the module can remain active when multiple strings are selected in the Editor. Set to `true` if your module is designed to handle multiple selected strings. If set to `false` or omitted, the right panel will be disabled when multiple strings are selected. | | `url` | **Type:** `string`**Required:** yes**Description:** The relative URL to the module content page that will be embedded in the Editor’s right panel. | | `environments` | **Type:** `string`**Allowed values:** `crowdin`, `crowdin-enterprise`**Description:** Set of environments where a module could be installed. This parameter is needed for cross-product applications. |
# Editor Translations Panel Module
> Create additional panels in the Editor's translation section
The module allows the creation of additional panels in the Editor’s translation section. When using this module in your Crowdin app, you can choose the Editor mode where you’d like the additional panels to be displayed.  ## [Access](#access) [Section titled “Access”](#access) You can grant access to this module to one of the following user categories: For Crowdin: * Only me (i.e., project owner) * Me, project managers and developers * All project members * Selected users For Crowdin Enterprise: * Only organization admins * Organization admins, project managers and developers * All users in the organization projects * Selected users ## [Structure](#structure) [Section titled “Structure”](#structure) manifest.json ```json { "modules": { "editor-translations-panel": [ { "key": "your-module-key", "name": "Module name", "modes": [ "translate" ], "url": "/editor-page" } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `name` | **Type:** `string`**Required:** yes**Description:** The human-readable name of the module. | | `modes` | **Type:** `array`**Required:** yes**Allowed values:** `assets`, `review`, `translate`**Description:** The Editor’s mode list where the module will be available. | | `url` | **Type:** `string`**Required:** yes**Description:** The relative URL to the content page of the module that will be integrated into the Crowdin Enterprise UI. | | `environments` | **Type:** `string`**Allowed values:** `crowdin`, `crowdin-enterprise`**Description:** Set of environments where a module could be installed. This parameter is needed for cross-product applications. |
# External QA Check Module
> Add an external QA check for advanced, AI-powered, and other specialized verifications.
This 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](/enterprise/project-settings/qa-checks/) or JavaScript-based [Custom QA checks](/enterprise/custom-qa-checks/). ## [Access](#access) [Section titled “Access”](#access) 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 ## [Structure](#structure) [Section titled “Structure”](#structure) manifest.json ```json { "modules": { "external-qa-check": [ { "key": "custom-check-qa", "name": "QA Check", "description": "Description", "runQaCheckUrl": "/validate", "getBatchSizeUrl": "/batch-size", "url": "/settings/index.html" } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `name` | **Type:** `string`**Required:** yes**Description:** The human-readable name of the module. | | `description` | **Type:** `string`**Description:** The human-readable description of what the module does. The description will be visible in the Crowdin Enterprise UI. | | `runQaCheckUrl` | **Type:** `string`**Required:** yes**Description:** The relative URL triggered when sending texts for QA validation. | | `getBatchSizeUrl` | **Type:** `string`**Required:** no**Description:** The relative URL triggered when retrieving the batch size supported by the module. | | `url` | **Type:** `string`**Required:** no**Description:** The relative URL to the module settings page. | | `environments` | **Type:** `string`**Allowed values:** `crowdin-enterprise`**Description:** Set of environments where a module could be installed. This parameter is needed for cross-product applications. | ## [Communication between External QA Check App and Crowdin](#communication-between-external-qa-check-app-and-crowdin) [Section titled “Communication between External QA Check App and Crowdin”](#communication-between-external-qa-check-app-and-crowdin) The system sends texts for QA validation using `runQaCheckUrl` either from the Editor after saving translation or when QA Checks are performed. The app then processes the texts and responds back to the system with one of the two possible types of responses: [without QA issues](#expected-response-from-the-app-without-qa-issues) or [with QA issues](#expected-response-from-the-app-with-qa-issues). When the app needs to determine the maximum batch size for the QA check, it sets the `getBatchSizeUrl` key in the `manifest.json` with the URL to an endpoint that provides the optimal batch size. ## [Request to the App from Crowdin for runQaCheckUrl](#request-to-the-app-from-crowdin-for-runqacheckurl) [Section titled “Request to the App from Crowdin for runQaCheckUrl”](#request-to-the-app-from-crowdin-for-runqacheckurl) Request payload example: ```json { "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, "rating": 0 } ], "strings": [ { "id": 1234567, "key": "update_success", "context": "Confirmation of successful software update", "maxLength": null, "text": "The update was successfully installed.", "fields": [] } ], "file": { "id": 123, "name": "filename.csv", "title": null, "context": null, "type": "csv", "path": "/filename.csv", "fields": [] } } } ``` ## [Expected Response from the App (Without QA issues)](#expected-response-from-the-app-without-qa-issues) [Section titled “Expected Response from the App (Without QA issues)”](#expected-response-from-the-app-without-qa-issues) Response payload example: ```json { "data": { "validations": [ { "translationId": 123, "passed": true } ] } } ``` ## [Expected Response from the App (With QA issues)](#expected-response-from-the-app-with-qa-issues) [Section titled “Expected Response from the App (With QA issues)”](#expected-response-from-the-app-with-qa-issues) Response payload example: ```json { "data": { "validations": [ { "translationId": 456, "passed": false, "error": { "message": "Example error message" } } ] } } ``` ## [Response from the App to Crowdin for getBatchSizeUrl](#response-from-the-app-to-crowdin-for-getbatchsizeurl) [Section titled “Response from the App to Crowdin for getBatchSizeUrl”](#response-from-the-app-to-crowdin-for-getbatchsizeurl) Response payload example: ```json { "data": { "size": 10 } } ``` The structure of the responses from the app should correspond to the presented examples; otherwise, Crowdin will consider them invalid.
# File Post-Export Processing Module
> Modify your files after exporting them from Crowdin
The post-export module allows you to modify your files after exporting them from Crowdin. With the post-export module, you can apply automated modifications to selected files. This module can work with a wide range of file formats, such as TXT, XML, JSON, and many more, to customize their contents. By using the post-export module in your Crowdin app, you can adjust the file format, structure, and content. Since the module is executed after Crowdin exports the file, you can fine-tune the content after the file is processed by the system. ## [Access](#access) [Section titled “Access”](#access) You can grant access to this module to one of the following user categories: For Crowdin: * Only me (i.e., project owner) * All project members * Selected users For Crowdin Enterprise: * Only organization admins * All users in the organization projects * Selected users ## [Structure](#structure) [Section titled “Structure”](#structure) manifest.json ```json { "modules": { "file-post-export": [ { "key": "your-post-export-module-key", "url": "/export-file", "signaturePatterns": { "fileName": "^.+\\.xml$", "fileContent": "\\s* " } } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `url` | **Type:** `string`**Required:** yes**Description:** The relative URL triggered on file export. | | `signaturePatterns` | **Type:** `object`**Description:** Contains `fileName` and/or `fileContent` regular expressions used to detect file type when exporting a translation file. | ## [Communication between File Processing App and Crowdin](#communication-between-file-processing-app-and-crowdin) [Section titled “Communication between File Processing App and Crowdin”](#communication-between-file-processing-app-and-crowdin) When exporting a file, Crowdin detects an appropriate module using the `signaturePatterns` parameter and makes an HTTP request to the app’s URL (`$baseUrl . $url`) for further processing. Additionally, during the file export, Crowdin will also validate the file name and content to ensure they match the appropriate file processing app modules. This process can include the pre-export processing module to modify the strings before the export and the post-export processing module to modify the content of the file after it is exported. To modify the file content, the system first locates the appropriate post-export module and sends the file content to it. The module then performs the predetermined modifications, which may include file format changes, structure, and content updates. Once the post-export module has completed the file modifications, Crowdin returns the modified file content, as well as a new file name or extension if applicable. ### [Request to the File Processing App](#request-to-the-file-processing-app) [Section titled “Request to the File Processing App”](#request-to-the-file-processing-app) Request payload example: ```json // max request payload - 5 MB // wait timeout - 2 minutes { "jobType": "file-post-export", "organization": { "id": 1, "domain": "{domain}", "baseUrl": "https://{domain}.crowdin.com", "apiBaseUrl": "https://{domain}.api.crowdin.com" }, "project": { "id": 1, "identifier": "your-project-identifier", "name": "Your Project Name" }, "file": { "id": 1, "name": "file.xml", "content": "VGhpcyBpcyBmaWxlIGNvbnRlbnQ=", // base64 encoded exported file content "contentUrl": "https://crowdin-tmp.downloads.crowdin.com/1/file.xml?aws-signature=...", // exported file public URL "rawContent": "VGhpcyBpcyBmaWxlIGNvbnRlbnQ=", // base64 encoded source file content "rawContentUrl": "https://crowdin-tmp.downloads.crowdin.com/1/file.xml?aws-signature=..." // source file public URL }, "sourceLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "pluralCategoryNames": ["one"], "pluralRules": "(n != 1)" }, "targetLanguages": [ { // same structure as for sourceLanguage, one element for export, can be more for multilingual files } ] } ``` Properties: | | | | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `jobType` | **Type:** `string`**Value:** `file-post-export`**Description:** Specifies the action of the file post-export module. | | `file.content`, `file.contentUrl` | **Type:** `string`**Description:** Parameters used to pass the base64 encoded exported file content (`file.content`) or a exported file public URL (`file.contentUrl`). Either of these two parameters can be used. | | `file.rawContent`, `file.rawContentUrl` | **Type:** `string`**Description:** Parameters used to pass the base64 encoded source file content (`file.rawContent`) or a source file public URL (`file.rawContentUrl`). Either of these two parameters can be used. | ### [Expected Response from the App](#expected-response-from-the-app) [Section titled “Expected Response from the App”](#expected-response-from-the-app) Response payload example: ```json // max response payload - 5 MB // wait timeout - 2 minutes { "data": { "content": "VGhpcyBpcyBmaWxlIGNvbnRlbnQ=", // base64 encoded modified file content "contentUrl": "https://crowdin-tmp.downloads.crowdin.com/1/file.xml?aws-signature=...", // modified file public URL "exportPattern": "file.html" // optional, new export pattern for a resulting file }, "error": { "message": "Your error message" } } ``` Properties: | | | | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `data.content`, `data.contentUrl` | **Type(data.content):** `string`**Type(data.contentUrl):** `string`**Description:** Parameters used to pass the base64 encoded modified file content (`data.content`) or a modified file public URL (`data.contentUrl`). Either of these two parameters can be used. | | `exportPattern` | **Type:** `string`**Description:** Optional parameter used to overwrite export pattern for a resulting file. | | `error.message` | **Type:** `string`**Description:** An error message that can be passed from the app to Crowdin and will be visible to a user in the UI. |
# File Post-Import Processing Module
> Modify the strings parsed from your files after the file import
The post-import module allows you to modify the strings parsed from your files after the file import. This module works with both the source strings and their respective translations. The pre-import module is especially useful if you’d like to preserve the original file structure and parse it as it is but want to make some changes to source strings, context, string translations or add labels, and maximum length limits for translations, etc. With the help of the post-import module, you will get a payload with the file content and strings parsed from it. Using the post-import module, you can modify the parsed strings in a number of ways (e.g., split, merge, or add new strings) and return the modified strings. This allows you to customize the strings without directly modifying the file content. ## [Access](#access) [Section titled “Access”](#access) You can grant access to this module to one of the following user categories: For Crowdin: * Only me (i.e., project owner) * All project members * Selected users For Crowdin Enterprise: * Only organization admins * All users in the organization projects * Selected users ## [Structure](#structure) [Section titled “Structure”](#structure) manifest.json ```json { "modules": { "file-post-import": [ { "key": "your-post-import-module-key", "url": "/import-strings", "signaturePatterns": { "fileName": "^.+\\.xml$", "fileContent": "\\s* " } } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `url` | **Type:** `string`**Required:** yes**Description:** The relative URL triggered on file import, update, and translation upload. | | `signaturePatterns` | **Type:** `object`**Description:** Contains `fileName` and/or `fileContent` regular expressions used to detect file type when uploading a new source file via UI (or via API without specified `type` parameter). | ## [Communication between File Processing App and Crowdin](#communication-between-file-processing-app-and-crowdin) [Section titled “Communication between File Processing App and Crowdin”](#communication-between-file-processing-app-and-crowdin) When importing a file, the system detects an appropriate post-import module using the `signaturePatterns` parameter and makes an HTTP request to the app’s URL (`$baseUrl . $url`) for further processing containing the extracted strings. The file processing app will modify the received strings according to your needs. The post-import module allows you to split, merge, add new strings, or edit the attributes of the existing ones. Once the modified strings are returned from the post-import module, they are added to the Crowdin project and become available for translation. ### [Request to the File Processing App](#request-to-the-file-processing-app) [Section titled “Request to the File Processing App”](#request-to-the-file-processing-app) Request payload example: ```json // max request payload - 5 MB // wait timeout - 2 minutes { "jobType": "file-post-import", "organization": { "id": 1, "domain": "{domain}", "baseUrl": "https://{domain}.crowdin.com", "apiBaseUrl": "https://{domain}.api.crowdin.com" }, "project": { "id": 1, "identifier": "your-project-identifier", "name": "Your Project Name" }, "file": { "id": 1, "name": "file.xml", "content": "VGhpcyBpcyBmaWxlIGNvbnRlbnQ=", // base64 encoded source file content "contentUrl": "https://crowdin-tmp.downloads.crowdin.com/1/file.xml?aws-signature=..." // source file public URL }, "sourceLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "pluralCategoryNames": ["one"], "pluralRules": "(n != 1)" }, "targetLanguages": [ { // same structure as for sourceLanguage, empty when uploading a new source file, one element for import_translations, can be more for multilingual files } ], "strings": [...], "stringsUrl": "https://tmp.downloads.crowdin.com/strings.ndjson", } ``` Properties: | | | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `jobType` | **Type:** `string`**Value:** `file-post-import`**Description:** Specifies the action of the file post-import module. | | `file.content`, `file.contentUrl` | **Type:** `string`**Description:** Parameters used to pass the base64 encoded source file content (`file.content`) or a source file public URL (`file.contentUrl`). Either of these two parameters can be used. | | `strings`, `stringsUrl` | **Type(strings):** `array`**Type(stringsUrl):** `string`**Description:** Parameters used for extracted strings after the file import. `strings` - strings array. `stringsUrl` - public URL to a [new-line delimited json](https://github.com/ndjson/ndjson-spec) with strings. Either of these two parameters can be used | ### [Expected Response from the App](#expected-response-from-the-app) [Section titled “Expected Response from the App”](#expected-response-from-the-app) Response payload example: ```json // max response payload - 5 MB // wait timeout - 2 minutes { "data": { "strings": [...], // modified strings array "stringsUrl": "https://app.example.com/jKe8ujs7a-segments.ndjson", // new-line delimited json file with modified strings "preview": "VGhpbmdzIGFyZSBvbmx5IGltcG9zc2libGUgdW50aWwgdGhleSdyZSBub3Qu", // optional, base64 encoded content of preview html file, not supported if there are plural strings "previewUrl": "https://app.example.com/LN3km2K6M-preview.html", // optional, URL of preview html file, not supported if there are plural strings }, "error": { "message": "Your error message" } } ``` Properties: | | | | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `data.strings`, `data.stringsUrl` | **Type(data.strings):** `array`**Type(data.stringsUrl):** `string`**Description:** Parameters used to pass the modified strings content. `data.strings` - modified strings array. `data.stringsUrl` - public URL to a [new-line delimited json](https://github.com/ndjson/ndjson-spec) with modified strings. Either of these two parameters can be used. | | `data.preview`, `data.previewUrl` | **Type(data.preview):** `string`**Type(data.previewUrl):** `string`**Description:** Parameters used to pass the optional HTML preview of the parsed strings content, which can be generated by the app. The generated HTML preview will be displayed in the Editor. See the [HTML Preview file example.](/developer/crowdin-apps-module-custom-file-format/#html-preview-of-the-file)CautionHTML preview won’t be displayed in the Crowdin Editor if the app passes strings with plurals. | | `error.message` | **Type:** `string`**Description:** An error message that can be passed from the app to Crowdin and will be visible to a user in the UI. | ### [Strings Array Structure](#strings-array-structure) [Section titled “Strings Array Structure”](#strings-array-structure) Below you can see an example of the strings that app will receive after the import. The same structure is used for the modified strings in the app response. Payload example: ```json // strings should be in "new-line delimited json" format if they passed by URL [ { // non plural string "uniqId": "9cdfb439c7876e703e307864c9167a15::1", // required, unique ID "identifier": "string-key-1", // required "context": "Some context", // optional "maxLength": 10, // optional, default null "isHidden": false, // optional, default null "hasPlurals": false, // optional, default false "labels": ["label-one", "label-two"], // optional, default [] "attributes": { "crowdinType": "json" }, "text": "String source text", // required "translations": { // optional "uk": { // targetLanguage.id "text": "Переклад стрічки", // required "status": "untranslated | translated | approved" // optional, default "translated" } // can be other languages for multilingual, check "targetLanguages" in the request payload } }, { // plural string "uniqId": "9cdfb439c7876e703e307864c9167a15::2", // required, unique ID "identifier": "string-key-2", "context": "Some optional context", "maxLength": 15, "isHidden": false, "hasPlurals": true, "labels": [], "text": { // keys from sourceLanguage.pluralCategoryNames "one": "One file", "other": "%d files" }, "translations": { "uk": { "text": { // keys from targetLanguage.pluralCategoryNames "one": "One file", "few": "%d файла", "many": "%d файлів" }, "status": { "one": "untranslated", "few": "translated", "many": "approved" } } } } ] ``` Properties: | | | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `uniqId` | **Type:** `string`**Description:** Unique identifier within the file. | | `identifier` | **Type:** `string`**Description:** Visible string key. | | `attributes.crowdinType` | **Type:** `string`**Allowed values:** Mostly match the `type` values accepted by the [Add File API method](/developer/api/v2/#operation/api.projects.files.post), excluding the following: `auto`, `xml`, `csv`, `docx`, `xlsx`, `dita`, `idml`, `mif`, `svg`.**Description:** Used to specify the file format type for a string when post-processing is needed. For example, if a JSON file contains strings with embedded HTML, setting `crowdinType` to `html` allows to re-parse the string using the HTML parser. This helps ensure correct rendering and prevents unwanted tags from being displayed. The value must match one of the supported Crowdin file types. |
# File Pre-Export Processing Module
> Modify the strings before the file export
The pre-export module allows you to modify the strings before the file export. This module works with both the source strings and their respective translations. The pre-export module is especially useful if you’d like to preserve the original file structure and export it as it is but want to make some changes to translated strings. With the help of the pre-export module, you will get a payload with the file content and strings for export. Using the pre-export module, you can modify translated strings and return them for export. This allows you to customize the strings without directly modifying the file content. ## [Access](#access) [Section titled “Access”](#access) You can grant access to this module to one of the following user categories: For Crowdin: * Only me (i.e., project owner) * All project members * Selected users For Crowdin Enterprise: * Only organization admins * All users in the organization projects * Selected users ## [Structure](#structure) [Section titled “Structure”](#structure) manifest.json ```json { "modules": { "file-pre-export": [ { "key": "your-pre-export-module-key", "url": "/export-strings", "signaturePatterns": { "fileName": "^.+\\.xml$", "fileContent": "\\s* " } } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `url` | **Type:** `string`**Required:** yes**Description:** The relative URL triggered on file export. | | `signaturePatterns` | **Type:** `object`**Description:** Contains `fileName` and/or `fileContent` regular expressions used to detect file type when exporting a translation file. | ## [Communication between File Processing App and Crowdin](#communication-between-file-processing-app-and-crowdin) [Section titled “Communication between File Processing App and Crowdin”](#communication-between-file-processing-app-and-crowdin) When exporting a file, the system detects an appropriate pre-export module using the `signaturePatterns` parameter and makes an HTTP request to the app’s URL (`$baseUrl . $url`) for further processing containing the source strings and their respective translations. The file processing app will modify the string translations according to your needs. Make sure that the pre-export module returns the original string quantity along with their string uniqId to ensure a successful translation export. Once the modified strings are returned from the pre-export module, they are used to build a translation file. ### [Request to the File Processing App](#request-to-the-file-processing-app) [Section titled “Request to the File Processing App”](#request-to-the-file-processing-app) Request payload example: ```json // max request payload - 5 MB // wait timeout - 2 minutes { "jobType": "file-pre-export", "organization": { "id": 1, "domain": "{domain}", "baseUrl": "https://{domain}.crowdin.com", "apiBaseUrl": "https://{domain}.api.crowdin.com" }, "project": { "id": 1, "identifier": "your-project-identifier", "name": "Your Project Name" }, "file": { "id": 1, "name": "file.xml", "content": "VGhpcyBpcyBmaWxlIGNvbnRlbnQ=", // base64 encoded source file content "contentUrl": "https://crowdin-tmp.downloads.crowdin.com/1/file.xml?aws-signature=..." // source file public URL }, "sourceLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "pluralCategoryNames": ["one"], "pluralRules": "(n != 1)" }, "targetLanguages": [ { // same structure as for sourceLanguage, one element for export, can be more for multilingual files } ], "strings": [...], "stringsUrl": "https://tmp.downloads.crowdin.com/strings.ndjson", } ``` Properties: | | | | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `jobType` | **Type:** `string`**Value:** `file-pre-export`**Description:** Specifies the action of the file pre-export module. | | `file.content`, `file.contentUrl` | **Type:** `string`**Description:** Parameters used to pass the base64 encoded source file content (`file.content`) or a source file public URL (`file.contentUrl`). Either of these two parameters can be used. | | `strings`, `stringsUrl` | **Type(strings):** `array`**Type(stringsUrl):** `string`**Description:** Parameters used for the source strings and their respective translations before the file export. `strings` - strings array. `stringsUrl` - public URL to a [new-line delimited json](https://github.com/ndjson/ndjson-spec) with strings. Either of these two parameters can be used. | ### [Expected Response from the App](#expected-response-from-the-app) [Section titled “Expected Response from the App”](#expected-response-from-the-app) Response payload example: ```json // max response payload - 5 MB // wait timeout - 2 minutes { "data": { "strings": [...], // modified strings array "stringsUrl": "https://app.example.com/jKe8ujs7a-segments.ndjson", // new-line delimited json file with modified strings }, "error": { "message": "Your error message" } } ``` Properties: | | | | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `data.strings`, `data.stringsUrl` | **Type(data.strings):** `array`**Type(data.stringsUrl):** `string`**Description:** Parameters used to pass the modified strings content. `data.strings` - modified strings array. `data.stringsUrl` - public URL to a [new-line delimited json](https://github.com/ndjson/ndjson-spec) with modified strings. Either of these two parameters can be used. | | `error.message` | **Type:** `string`**Description:** An error message that can be passed from the app to Crowdin and will be visible to a user in the UI. | ### [Strings Array Structure](#strings-array-structure) [Section titled “Strings Array Structure”](#strings-array-structure) Below you can see an example of the strings that app will receive before the export. The same structure is used for the modified strings in the app response. Payload example: ```json // strings should be in "new-line delimited json" format if they passed by URL [ { // non plural string "uniqId": "9cdfb439c7876e703e307864c9167a15::1", // required, unique ID "identifier": "string-key-1", // required "context": "Some context", // optional "maxLength": 10, // optional, default null "isHidden": false, // optional, default null "hasPlurals": false, // optional, default false "labels": ["label-one", "label-two"], // optional, default [] "text": "String source text", // required "translations": { // optional "uk": { // targetLanguage.id "text": "Переклад стрічки", // required "status": "untranslated | translated | approved" // optional, default "translated" } // can be other languages for multilingual, check "targetLanguages" in the request payload } }, { // plural string "uniqId": "9cdfb439c7876e703e307864c9167a15::2", // required, unique ID "identifier": "string-key-2", "context": "Some optional context", "maxLength": 15, "isHidden": false, "hasPlurals": true, "labels": [], "text": { // keys from sourceLanguage.pluralCategoryNames "one": "One file", "other": "%d files" }, "translations": { "uk": { "text": { // keys from targetLanguage.pluralCategoryNames "one": "One file", "few": "%d файла", "many": "%d файлів" }, "status": { "one": "untranslated", "few": "translated", "many": "approved" } } } } ] ``` Properties: | | | | ------------ | --------------------------------------------------------------------- | | `uniqId` | **Type:** `string`**Description:** Unique identifier within the file. | | `identifier` | **Type:** `string`**Description:** Visible string key. |
# File Pre-Import Processing Module
> Modify your files before importing them to Crowdin
The pre-import module allows you to modify your files before importing them to Crowdin. With the pre-import module, you can apply automated modifications to selected files. This module can work with a wide range of file formats, such as TXT, XML, JSON, and many more, to customize their contents. By using the pre-import module in your Crowdin app, you can adjust the file format, structure, and content. Since the module is executed before Crowdin imports the file, you can fine-tune the content before the file is processed by the system. ## [Access](#access) [Section titled “Access”](#access) You can grant access to this module to one of the following user categories: For Crowdin: * Only me (i.e., project owner) * All project members * Selected users For Crowdin Enterprise: * Only organization admins * All users in the organization projects * Selected users ## [Structure](#structure) [Section titled “Structure”](#structure) manifest.json ```json { "modules": { "file-pre-import": [ { "key": "your-pre-import-module-key", "url": "/import-file", "signaturePatterns": { "fileName": "^.+\\.xml$", "fileContent": "\\s* " } } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `url` | **Type:** `string`**Required:** yes**Description:** The relative URL triggered on file import, update, and translation upload. | | `signaturePatterns` | **Type:** `object`**Description:** Contains `fileName` and/or `fileContent` regular expressions used to detect file type when uploading a new source file via UI (or via API without specified `type` parameter). | ## [Communication between File Processing App and Crowdin](#communication-between-file-processing-app-and-crowdin) [Section titled “Communication between File Processing App and Crowdin”](#communication-between-file-processing-app-and-crowdin) When importing a file, Crowdin detects an appropriate module using the `signaturePatterns` parameter and makes an HTTP request to the app’s URL (`$baseUrl . $url`) for further processing. Additionally, during the file import, Crowdin will also validate the file name and content to ensure they match the appropriate file processing app modules. This process can include the pre-import processing module to modify the content of the file before it is imported and the post-import processing module to modify the strings extracted from the file. To modify the file content, the system first locates the appropriate pre-import module and sends the file content to it. The module then performs the predetermined modifications, which may include file format changes, structure, and content updates. Once the pre-import module has completed the file modifications, Crowdin receives the modified file content, as well as a new file name or extension if applicable. ### [Request to the File Processing App](#request-to-the-file-processing-app) [Section titled “Request to the File Processing App”](#request-to-the-file-processing-app) Request payload example: ```json // max request payload - 5 MB // wait timeout - 2 minutes { "jobType": "file-pre-import", "organization": { "id": 1, "domain": "{domain}", "baseUrl": "https://{domain}.crowdin.com", "apiBaseUrl": "https://{domain}.api.crowdin.com" }, "project": { "id": 1, "identifier": "your-project-identifier", "name": "Your Project Name" }, "file": { "id": 1, "name": "file.xml", "content": "VGhpcyBpcyBmaWxlIGNvbnRlbnQ=", // base64 encoded source file content "contentUrl": "https://crowdin-tmp.downloads.crowdin.com/1/file.xml?aws-signature=..." // source file public URL }, "sourceLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "pluralCategoryNames": [ "one" ], "pluralRules": "(n != 1)" }, "targetLanguages": [ { // same structure as for sourceLanguage, empty when uploading a new source file, one element for import_translations, can be more for multilingual files } ] } ``` Properties: | | | | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `jobType` | **Type:** `string`**Value:** `file-pre-import`**Description:** Specifies the action of the file pre-import module. | | `file.content`, `file.contentUrl` | **Type:** `string`**Description:** Parameters used to pass the base64 encoded source file content (`file.content`) or a source file public URL (`file.contentUrl`). Either of these two parameters can be used. | ### [Expected Response from the App](#expected-response-from-the-app) [Section titled “Expected Response from the App”](#expected-response-from-the-app) Response payload example: ```json // max response payload - 5 MB // wait timeout - 2 minutes { "data": { "content": "VGhpcyBpcyBmaWxlIGNvbnRlbnQ=", // base64 encoded modified file content "contentUrl": "https://crowdin-tmp.downloads.crowdin.com/1/file.xml?aws-signature=...", // modified file public URL "fileName": "file.html", // optional, new file name with extension "fileType": "webxml" // optional, an importer Crowdin should use to import a file }, "error": { "message": "Your error message" } } ``` Properties: | | | | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `data.content`, `data.contentUrl` | **Type(data.content):** `string`**Type(data.contentUrl):** `string`**Description:** Parameters used to pass the base64 encoded modified file content (`data.content`) or a modified file public URL (`data.contentUrl`). Either of these two parameters can be used. | | `fileName` | **Type:** `string`**Description:** Optional parameter used to overwrite a file name and extension with a new one. | | `fileType` | **Type:** `string`**Description:** Optional parameter to specify an importer Crowdin should use to import a file. | | `error.message` | **Type:** `string`**Description:** An error message that can be passed from the app to Crowdin and will be visible to a user in the UI. |
# Modal Module
> Create a custom modal dialog to open from the Context Menu module
The module allows the creation of a new modal dialog. The current module works only with the Context menu module that opens it. ## [Access](#access) [Section titled “Access”](#access) You can grant access to this module to one of the following user categories: For Crowdin: * Only me (i.e., project owner) * All project members * Selected users For Crowdin Enterprise: * Only organization admins * All users in the organization projects * Selected users ## [Structure](#structure) [Section titled “Structure”](#structure) manifest.json ```json { "modules": { "modal": [ { "key": "your-module-key", "name": "Module name", "url": "/module-url" } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `name` | **Type:** `string`**Required:** yes**Description:** The human-readable name of the module. Uses as Context menu text | | `url` | **Type:** `string`**Required:** yes**Description:** The relative URL to the content page of the module that will be integrated into the UI. | | `environments` | **Type:** `string`**Allowed values:** `crowdin`, `crowdin-enterprise`**Description:** Define the environment in which the module will run. This parameter is needed for cross-product applications. |
# Organization Menu Module
> Create a new section in the left panel of the Workspace home page
The module allows the creation of a new section in the left panel of the Workspace home page.  ## [Access](#access) [Section titled “Access”](#access) 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 ## [Structure](#structure) [Section titled “Structure”](#structure) manifest.json ```json { "modules": { "organization-menu": [ { "key": "your-module-key", "name": "Module name", "url": "/organization-page", "icon": "/images/icon.png" } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `name` | **Type:** `string`**Required:** yes**Description:** The human-readable name of the module. | | `url` | **Type:** `string`**Required:** yes**Description:** The relative URL to the content page of the module that will be integrated into the Crowdin Enterprise UI. | | `icon` | **Type:** `string`**Required:** yes**Description:** The relative URL to the new section’s icon that will be displayed in the Crowdin Enterprise UI. The recommended resolution is 24x24 pixels. |
# Organization Menu (Crowdsource View) Module
> Create additional tabs on the crowdsourcing public page in Crowdin Enterprise
The module allows the creation of additional tabs on the crowdsourcing public page in Crowdin Enterprise. To work with it, ensure that you have at least one project containing the Crowdsourcing workflow step and that it’s published on the Crowdsourcing settings page. ## [Access](#access) [Section titled “Access”](#access) 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 * Guests (unauthenticated users) ## [Structure](#structure) [Section titled “Structure”](#structure) manifest.json ```json { "modules": { "organization-menu-crowdsource": [ { "key": "your-module-key", "name": "Module name", "url": "/crowdsource-page" } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `name` | **Type:** `string`**Required:** yes**Description:** The human-readable name of the module. | | `url` | **Type:** `string`**Required:** yes**Description:** The relative URL to the content page of the module that will be integrated into the Crowdin Enterprise UI. |
# Organization Settings Menu Module
> Create a new section in the Organization Settings page
The module allows the creation of a new section in the Organization Settings page.  ## [Access](#access) [Section titled “Access”](#access) You can grant access to this module to one of the following user categories: * Only organization admins * Selected organization admins ## [Structure](#structure) [Section titled “Structure”](#structure) manifest.json ```json { "modules": { "organization-settings-menu": [ { "key": "your-settings-module-key", "name": "Settings Module name", "url": "/organization-settings-page", "icon": "/images/settings-icon.png" } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `name` | **Type:** `string`**Required:** yes**Description:** The human-readable name of the module. | | `url` | **Type:** `string`**Required:** yes**Description:** The relative URL to the settings page of the module that will be integrated into the Crowdin Enterprise UI. | | `icon` | **Type:** `string`**Required:** yes**Description:** The relative URL to the settings section’s icon that will be displayed in the Crowdin Enterprise UI. The recommended resolution is 24x24 pixels. |
# Resources Module
> Create a new item in the Extensions section of the user's profile
The module allows the creation of a new item in the **Extensions** section of the user’s profile.  ## [Access](#access) [Section titled “Access”](#access) You can grant access to this module to one of the following user categories: * Only me (i.e., project owner) * All project members * Selected users ## [Structure](#structure) [Section titled “Structure”](#structure) manifest.json ```json { "modules": { "profile-resources-menu": [ { "key": "your-module-key", "name": "Module name", "url": "/resource-page", "icon": "/images/icon.png", "environments": "crowdin" } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `name` | **Type:** `string`**Required:** yes**Description:** The human-readable name of the module. | | `url` | **Type:** `string`**Required:** yes**Description:** The relative URL to the content page of the module that will be integrated into the Crowdin UI. | | `icon` | **Type:** `string`**Required:** yes**Description:** The relative URL to the icon that will be displayed in the Crowdin UI. The recommended resolution is 24x24 pixels. | | `environments` | **Type:** `string`**Allowed values:** `crowdin`**Description:** Set of environments where a module could be installed. This parameter is needed for cross-product applications. |
# Profile Settings Menu Module
> Create a new section in the user's profile settings
The module allows the creation of a new section in the user’s profile settings.  ## [Access](#access) [Section titled “Access”](#access) You can grant access to this module to one of the following user categories: * Only me (i.e., profile owner) ## [Structure](#structure) [Section titled “Structure”](#structure) manifest.json ```json { "modules": { "profile-settings-menu": [ { "key": "your-settings-module-key", "name": "Settings Module name", "url": "/profile-settings-page" } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `name` | **Type:** `string`**Required:** yes**Description:** The human-readable name of the module. | | `url` | **Type:** `string`**Required:** yes**Description:** The relative URL to the settings page of the module that will be integrated into the Crowdin UI. |
# Integrations Module
> Create a new integration within the Crowdin project
The module allows the creation and insertion of a new integration within the Crowdin project. You can find it in the **Integrations** section of the project page.  ## [Access](#access) [Section titled “Access”](#access) You can grant access to this module to one of the following user categories: For Crowdin: * Only me (i.e., project owner) * All project members * Selected users For Crowdin Enterprise: * Only organization admins * All users in the organization projects * Selected users ## [Structure](#structure) [Section titled “Structure”](#structure) manifest.json ```json { "modules": { "project-integrations": [ { "key": "your-module-key", "name": "Module name", "description": "Module description", "logo": "/logo.png", "url": "/integration-page" } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `name` | **Type:** `string`**Required:** yes**Description:** The human-readable name of the module. | | `description` | **Type:** `string`**Description:** The human-readable description of what the module does. The description will be visible in the Crowdin Enterprise UI. | | `logo` | **Type:** `string`**Required:** yes**Description:** The relative URL to the integration’s logo that will be displayed in the Crowdin Enterprise UI. The recommended resolution is 48x48 pixels. | | `url` | **Type:** `string`**Required:** yes**Description:** The relative URL to the content page of the module that will be integrated into the Crowdin Enterprise UI. | | `environments` | **Type:** `string`**Allowed values:** `crowdin`, `crowdin-enterprise`**Description:** Set of environments where a module could be installed. This parameter is needed for cross-product applications. |
# Project Menu Module
> Create a new section in the project page
The module allows the creation of a new section in the project page.  ## [Access](#access) [Section titled “Access”](#access) You can grant access to this module to one of the following user categories: For Crowdin: * Only me (i.e., project owner) * Me, project managers and developers * All project members * Selected users For Crowdin Enterprise: * Only organization admins * Organization admins, project managers and developers * All users in the organization projects * Selected users ## [Structure](#structure) [Section titled “Structure”](#structure) manifest.json ```json { "modules": { "project-menu": [ { "key": "your-module-key", "name": "Module name", "url": "/project-page" } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `name` | **Type:** `string`**Required:** yes**Description:** The human-readable name of the module. | | `url` | **Type:** `string`**Required:** yes**Description:** The relative URL to the content page of the module that will be integrated into the Crowdin Enterprise UI. | | `environments` | **Type:** `string`**Allowed values:** `crowdin`, `crowdin-enterprise`**Description:** Set of environments where a module could be installed. This parameter is needed for cross-product applications. |
# Project Menu (Crowdsource View) Module
> Create additional tabs on the crowdsourcing public page of the project in Crowdin Enterprise
The module allows the creation of additional tabs on the crowdsourcing public page of the project in Crowdin Enterprise. To work with it, ensure that your project meets the following requirements: * The project’s workflow contains the Crowdsourcing step. * The project is published on the Crowdsourcing settings page. ## [Access](#access) [Section titled “Access”](#access) You can grant access to this module to one of the following user categories: For Crowdin: * Only me (i.e., project owner) * All project members * Selected users * Guests (unauthenticated users) For Crowdin Enterprise: * Only organization admins * All users in the organization projects * Selected users * Guests (unauthenticated users) ## [Structure](#structure) [Section titled “Structure”](#structure) manifest.json ```json { "modules": { "project-menu-crowdsource": [ { "key": "your-module-key", "name": "Module name", "url": "/crowdsource-page" } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `name` | **Type:** `string`**Required:** yes**Description:** The human-readable name of the module. | | `url` | **Type:** `string`**Required:** yes**Description:** The relative URL to the content page of the module that will be integrated into the Crowdin Enterprise UI. |
# Reports Module
> Create a new report within the Crowdin or Crowdin Enterprise project
The module allows the creation and insertion of a new report within the Crowdin or Crowdin Enterprise project. You can find it in the **Reports** section. ## [Access](#access) [Section titled “Access”](#access) You can grant access to this module to one of the following user categories: For Crowdin: * Only me (i.e., project owner) * All project members * Selected users For Crowdin Enterprise: * Only organization admins * All users in the organization projects * Selected users ## [Structure](#structure) [Section titled “Structure”](#structure) manifest.json ```json { "modules": { "project-reports": [ { "key": "your-module-key", "name": "Module name", "description": "Module description", "logo": "/logo.png", "url": "/reports-page" } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `name` | **Type:** `string`**Required:** yes**Description:** The human-readable name of the module. | | `description` | **Type:** `string`**Description:** The human-readable description of what the module does. The description will be visible in the Crowdin Enterprise UI. | | `logo` | **Type:** `string`**Required:** yes**Description:** The relative URL to the tool’s logo that will be displayed in the Crowdin Enterprise UI. The recommended resolution is 48x48 pixels. | | `url` | **Type:** `string`**Required:** yes**Description:** The relative URL to the content page of the module that will be integrated into the Crowdin Enterprise UI. | | `environments` | **Type:** `string`**Allowed values:** `crowdin`, `crowdin-enterprise`**Description:** Set of environments where a module could be installed. This parameter is needed for cross-product applications. |
# Tools Module
> Create a new tool page within the Crowdin or Crowdin Enterprise project
The tools module allows the creation and insertion of a new tool page within the Crowdin or Crowdin Enterprise project. You can find it in the **Tools** section.  ## [Access](#access) [Section titled “Access”](#access) You can grant access to this module to one of the following user categories: For Crowdin: * Only me (i.e., project owner) * All project members * Selected users For Crowdin Enterprise: * Only organization admins * All users in the organization projects * Selected users ## [Structure](#structure) [Section titled “Structure”](#structure) manifest.json ```json { "modules": { "project-tools": [ { "key": "your-module-key", "name": "Module name", "description": "Module description", "logo": "/logo.png", "url": "/tools-page" } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `name` | **Type:** `string`**Required:** yes**Description:** The human-readable name of the module. | | `description` | **Type:** `string`**Description:** The human-readable description of what the module does. The description will be visible in the Crowdin Enterprise UI. | | `logo` | **Type:** `string`**Required:** yes**Description:** The relative URL to the tool’s logo that will be displayed in the Crowdin Enterprise UI. The recommended resolution is 48x48 pixels. | | `url` | **Type:** `string`**Required:** yes**Description:** The relative URL to the content page of the module that will be integrated into the Crowdin Enterprise UI. | | `environments` | **Type:** `string`**Allowed values:** `crowdin`, `crowdin-enterprise`**Description:** Set of environments where a module could be installed. This parameter is needed for cross-product applications. |
# Translation Alignment Processing Module
> Use a custom service to align translations for non-key-value files
The Translation Alignment Processing module allows you to override Crowdin’s default alignment mechanism when uploading translations for non-key-value files (e.g., HTML, MD). By default, Crowdin automatically aligns uploaded translations with existing source strings. When you use this module in your Crowdin app, you can intercept this process and apply your own custom alignment logic, for example, by using a third-party alignment service. This gives you full control over how translations are mapped to their corresponding source strings for complex document formats. ## [Access](#access) [Section titled “Access”](#access) You can grant access to this module to one of the following user categories: For Crowdin: * Only me (i.e., project owner) * Me, project managers and developers * Selected users For Crowdin Enterprise: * Only organization admins * Organization admins, project managers and developers * Selected users ## [Structure](#structure) [Section titled “Structure”](#structure) manifest.json ```json { "modules": { "file-translations-alignment": [ { "key": "your-translations-alignment-module-key", "url": "/translations-alignment", "signaturePatterns": { "fileName": "^.+\\.html$", "fileContent": "" } } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `url` | **Type:** `string`**Required:** yes**Description:** The relative URL triggered on translation upload. | | `signaturePatterns` | **Type:** `object`**Description:** Contains `fileName` and/or `fileContent` regular expressions used to detect file type when uploading translations via UI (or via API without specified `type` parameter). | ## [Communication between Translation Alignment App and Crowdin](#communication-between-translation-alignment-app-and-crowdin) [Section titled “Communication between Translation Alignment App and Crowdin”](#communication-between-translation-alignment-app-and-crowdin) When uploading a translation file for a non-key-value format, the system detects an appropriate translation alignment module using the `signaturePatterns` parameter. Crowdin then makes an HTTP request to the app’s URL (`$baseUrl . $url`). This request contains two main arrays: `sourceStrings` (from the project) and `translationStrings` (parsed from the uploaded translation file). The app’s job is to process these two sets of strings, perform the alignment, and return a structured response that maps the translations to their correct source string identifiers. ### [Request to the Translation Alignment App](#request-to-the-translation-alignment-app) [Section titled “Request to the Translation Alignment App”](#request-to-the-translation-alignment-app) Request payload example: ```json // max request payload - 5 MB // wait timeout - 2 minutes { "jobType": "translation-alignment-file", "organization": { "id": 1, "domain": "{domain}", "baseUrl": "https://{domain}.crowdin.com", "apiBaseUrl": "https://{domain}.api.crowdin.com" }, "project": { "id": 1, "identifier": "your-project-identifier", "name": "Your Project Name", "userId": 1, "sourceLanguageId": "en", "targetLanguageIds": [ "ja", "uk", "et" ], "createdAt": "2030-01-15T07:00:00+00:00", "updatedAt": "2030-01-15T07:00:15+00:00", "lastActivity": "2030-01-15T10:12:13+00:00", "description": null, "url": "/project/your-project-identifier", "cname": null }, "sourceLanguage": { "id": "en", "name": "English", "editorCode": "en", "twoLettersCode": "en", "threeLettersCode": "eng", "locale": "en-US", "androidCode": "en-rUS", "osxCode": "en.lproj", "osxLocale": "en", "pluralCategoryNames": [ "one", "other" ], "pluralRules": "(n != 1)", "pluralExamples": [ "1", "0, 2-999; 1.2, 2.07..." ], "textDirection": "ltr", "dialectOf": null }, "targetLanguages": [ { // Language object for the translation being aligned. Has the same structure as sourceLanguage. } ], "file": { "id": 4219, "name": "en.html", "title": null, "path": "/en.html", "type": "html32", "isMultilingual": false, "status": "active", "revision": 1, "branchId": null, "directoryId": 0 }, "sourceStrings": [ { "id": 1234567, "text": "Welcome!", "context": "Document Title\\r\\nXPath: \\/html\\/head\\/title" } // other source strings ... ], "translationStrings": [ { "id": null, "text": "Ласкаво просимо!", "context": "Document Title\\r\\nXPath: \\/html\\/head\\/title" } // other translation strings ... ] } ``` Properties: | | | | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `jobType` | **Type:** `string`**Value:** `translation-alignment-file`**Description:** Specifies the action of the translation alignment module. | | `file` | **Type:** `object`**Description:** Contains information about the source file in Crowdin to which the translations are being uploaded. | | `targetLanguages` | **Type:** `array`**Description:** An array containing the language object of the translation file being uploaded and aligned. | | `sourceStrings`, `sourceStringsUrl` | **Type(sourceStrings):** `array`**Type(sourceStringsUrl):** `string`**Description:** Parameters used for source strings from the original file. `sourceStrings` - array of source string objects. `sourceStringsUrl` - public URL to a [new-line delimited json](https://github.com/ndjson/ndjson-spec) with source strings. Either of these two parameters can be used. | | `translationStrings`, `translationStringsUrl` | **Type(translationStrings):** `array`**Type(translationStringsUrl):** `string`**Description:** Parameters used for strings parsed from the uploaded translation file. `translationStrings` - array of simple string objects. `translationStringsUrl` - public URL to a [new-line delimited json](https://github.com/ndjson/ndjson-spec) with translation strings. Either of these two parameters can be used. | ### [Expected Response from the App](#expected-response-from-the-app) [Section titled “Expected Response from the App”](#expected-response-from-the-app) The app must perform its custom alignment and return a response containing a `translations` array. This array maps each translation text to its corresponding `sourceStringId`. Response payload example: ```json // max response payload - 5 MB // wait timeout - 2 minutes { "data": { "translations": [ { "sourceStringId": 1234567, "text": "Ласкаво просимо!" }, // other translation mappings... ], // or use translationsUrl for large payloads "translationsUrl": "https://app.example.com/jKe8ujs7a-aligned.ndjson" // new-line delimited json file with aligned translations }, "error": { "message": "Optional error message" } } ``` Properties: | | | | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `data.translations`, `data.translationsUrl` | **Type(data.translations):** `array`**Type(data.translationsUrl):** `string`**Description:** Parameters used to pass the aligned translations. `data.translations` - array of alignment objects. `data.translationsUrl` - public URL to a [new-line delimited json](https://github.com/ndjson/ndjson-spec) with alignment objects. Either of these two parameters can be used. | | `data.translations.sourceStringId` | **Type:** `integer`**Description:** The numeric identifier of the source string to which the translation should be applied. | | `data.translations.text` | **Type:** `string` or `object`**Description:** The translation text to be applied. For plural strings, this must be an object keyed by the target language’s plural category names (e.g., `one`, `other`). | | `error.message` | **Type:** `string`**Description:** An error message that can be passed from the app to Crowdin and will be visible to a user in the UI. |
# Webhook
> Subscribe
The Webhook module allows Crowdin Apps to listen for specific events and trigger actions when those events occur. This module is useful for automating workflows (e.g., work in combination with [Workflow Step Type module](/developer/crowdin-apps-module-workflow-step-type/)), synchronizing external systems, or logging project activities. ## [Structure](#structure) [Section titled “Structure”](#structure) manifest.json ```json { "modules": { "webhook": [ { "key": "webhook-key-module-key", "url": "/webhooks", "events": [ "project.translated", "project.approved" ], "environments": [ "crowdin", "crowdin-enterprise" ] } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `url` | **Type:** `string`**Required:** yes**Description:** The relative URL of the webhook handler in the app. | | `events` | **Type:** `array`**Required:** yes**Description:** A list of events that trigger the webhook. See the list of [available webhook events](/developer/webhooks/#events). | | `environments` | **Type:** `string`**Allowed values:** `crowdin`, `crowdin-enterprise`**Description:** Set of environments where a module could be installed. This parameter is needed for cross-product applications. | ## [Communication between App and Crowdin](#communication-between-app-and-crowdin) [Section titled “Communication between App and Crowdin”](#communication-between-app-and-crowdin) All webhooks are triggered via an HTTP POST request with a JSON-formatted payload. The request may contain a single event or multiple events (always delivered in bulk mode). Read more about [Webhook Payload Examples](/developer/webhooks/#webhook-payload-examples). ### [Request to the App](#request-to-the-app) [Section titled “Request to the App”](#request-to-the-app) When an event occurs, Crowdin sends an HTTP POST request to your webhook endpoint, complete with necessary headers for authentication and security. Below is an example of such a request. **HTTP request:** ```plaintext POST /webhooks HTTP/1.1 User-Agent: Crowdin-Webhook Content-Type: application/json X-Crowdin-Id: X-Crowdin-Domain: X-Crowdin-Signature: X-Module-Key: ``` **Request Body:** ```json { "events": [ { "event": "project.translated", "project": { "id": "777", // ... }, "targetLanguage": { "id": "uk", // ... } }, { "event": "project.approved", "project": { "id": "777", // ... }, "targetLanguage": { "id": "uk", // ... } } ] } ``` During the app installation process, a unique secret called the [`app_secret`](/developer/crowdin-apps-installation/#installed-event-communication-flow) is provided to your app. This secret is used to generate the webhook signature using the HMAC algorithm with SHA-256. ### [Expected Response from the App](#expected-response-from-the-app) [Section titled “Expected Response from the App”](#expected-response-from-the-app) Crowdin expects your app to respond with a successful HTTP status code (i.e., any 2XX status) to acknowledge receipt of the webhook payload.
# Workflow Step Type
> Create a new workflow step type for Crowdin Enterprise workflows
This module allows you to create custom workflow step types to extend the default list of workflow steps in Crowdin Enterprise. With this app installed, the new workflow steps type become available in the workflow editor, where they can be added to workflows and templates, enabling greater customization and flexibility. ## [Access](#access) [Section titled “Access”](#access) 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 ## [Structure](#structure) [Section titled “Structure”](#structure) manifest.json ```json { "modules": { "workflow-step-type": [ { "key": "custom-workflow-step", "name": "Custom Workflow Step", "logo": "/logo.png", "description": "A sample custom step for Crowdin Enterprise workflows", "boundaries": { "input": { "title": "Input Strings", "ports": [ "untranslated", "translated", "approved", "all", "false", "true", "skipped", "initial" ] }, "outputs": [ { "title": "Processed Strings", "port": "translated" }, { "title": "Unprocessed Strings", "port": "untranslated" } ] }, "editorMode": "comfortable", "updateSettingsUrl": "/settings/custom-workflow-step", "deleteSettingsUrl": "/delete/custom-workflow-step", "url": "/workflow-step/custom-workflow-step", "environments": [ "crowdin-enterprise" ] } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `name` | **Type:** `string`**Required:** yes**Description:** The human-readable name of the module. | | `logo` | **Type:** `string`**Required:** yes**Description:** The relative URL to the custom AI’s logo that will be displayed in the Crowdin Enterprise UI. The recommended resolution is 48x48 pixels. | | `description` | **Type:** `string`**Required:** yes**Description:** The human-readable description of what the module does. The description will be visible in the Crowdin Enterprise UI. | | `boundaries` | **Type:** `object`**Required:** yes**Description:** Defines the input and output ports for the workflow step, determining how strings enter and exit the step. | | `boundaries.input` | **Type:** `object`**Required:** yes**Description:** Specifies the properties of the input data for the workflow step, including available ports. | | `boundaries.input.title` | **Type:** `string`**Required:** yes**Description:** The title for the input section of the workflow step. | | `boundaries.input.ports` | **Type:** `array`**Required:** yes**Allowed values:** `untranslated`, `translated`, `approved`, `all`, `false`, `true`, `skipped`, `initial`**Description:** Defines the string statuses that can be processed by this workflow step. | | `boundaries.outputs` | **Type:** `array`**Required:** yes**Description:** Specifies the possible outputs of the workflow step, determining how processed strings move forward. | | `boundaries.outputs.[]` | **Type:** `object`**Required:** yes**Description:** Defines the outputs of the workflow step. Each object in the array contains:- `title` (**string**) – The title for the output section of the workflow step. - `port` (**string**) – The port type used for connecting outputs. | | `editorMode` | **Type:** `string`**Required:** no**Allowed values:** `side-by-side`, `comfortable`, `multilingual`**Description:** Defines the Crowdin Enterprise Editor mode for this workflow step. | | `updateSettingsUrl` | **Type:** `string`**Required:** no**Description:** The relative URL for sending updated workflow step settings after a user saves changes in the workflow editor. Used if the custom workflow step has a configuration. | | `deleteSettingsUrl` | **Type:** `string`**Required:** no**Description:** The relative URL for deleting the workflow step in the workflow editor. | | `url` | **Type:** `string`**Required:** no**Description:** The relative URL to the iframe for configuring the workflow step settings. | | `environments` | **Type:** `string`**Allowed values:** `crowdin-enterprise`**Description:** Set of environments where a module could be installed. This parameter is needed for cross-product applications. | ## [Communication Between App and Crowdin](#communication-between-app-and-crowdin) [Section titled “Communication Between App and Crowdin”](#communication-between-app-and-crowdin) The Workflow Step Type module relies on webhooks and API methods to communicate with Crowdin Enterprise. Apps that include this module must also define the [Webhook module](/developer/crowdin-apps-module-webhook/) to receive string-related events (i.e., `string.status_on_step.recalculation_triggered`) and process them accordingly. ### [Receiving Webhook Events from Crowdin](#receiving-webhook-events-from-crowdin) [Section titled “Receiving Webhook Events from Crowdin”](#receiving-webhook-events-from-crowdin) Crowdin Enterprise periodically sends a batched webhook payload to the app’s Webhook module whenever strings reach a custom workflow step provided by the app. This payload contains the `string.status_on_step.recalculation_triggered` event and includes all relevant strings that need external processing (e.g., AI-based proofreading). Example webhook payload for the `string.status_on_step.recalculation_triggered` event: ```json { "events": [ { "event": "string.status_on_step.recalculation_triggered", "stringStatus": { "status": "NEED_PROCESS", "output": "", "organizationId": "200007777", "translation": { "id": 1106423, "identifier": "058eb6ea2bdcc79a6a7208783c8bfb50", "key": "string_1", "text": "Not all videos are shown to users. See more", "type": "text", "context": "string_1", "maxLength": "50", "isHidden": false, "isDuplicate": false, "masterStringId": null, "revision": 1, "hasPlurals": false, "labelIds": [], "url": "https://umbrella.crowdin.com/editor/173/743/en-et#1106423", "createdAt": "2024-10-29T10:47:13+00:00", "updatedAt": null, "file": { "id": 743, "name": "umbrella_app.xml", "title": null, "type": "android8", "path": "/umbrella_app.xml", "status": "active", "revision": "1", "branch": { "id": null }, "directory": { "id": null }, "project": null }, "project": { "id": 173, "userId": 1, "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "et" ], "identifier": "d3026ae4cff9820bc140a210d23b35ad", "name": "Project Name", "createdAt": "2024-10-25T14:37:47+00:00", "updatedAt": "2024-10-25T14:37:47+00:00", "lastActivity": "2025-01-30T09:32:58+00:00", "description": "", "url": "https://umbrella.crowdin.com/u/projects/173", "cname": null, "languageAccessPolicy": null, "visibility": null, "publicDownloads": null, "logo": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJY......BBQmCC", "isExternal": false, "externalType": null, "hasCrowdsourcing": false, "groupId": 1 } }, "sourceLanguage": { "id": "en", "name": "English", "editorCode": "en", "twoLettersCode": "en", "threeLettersCode": "eng", "locale": "en-US", "androidCode": "en-rUS", "osxCode": "en.lproj", "osxLocale": "en", "textDirection": "ltr", "dialectOf": null }, "affectedLanguage": { "id": "et", "name": "Estonian", "editorCode": "et", "twoLettersCode": "et", "threeLettersCode": "est", "locale": "et-EE", "androidCode": "et-rEE", "osxCode": "et.lproj", "osxLocale": "et", "textDirection": "ltr", "dialectOf": null }, "workflowStep": { "id": 1035, "title": "AI Review", "type": "Application", "languages": [], "applicationModule": { "applicationIdentifier": "custom-workflow-step", "moduleKey": "review-step" } }, "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "https://avatar-url.com/avatar/1/small/1bc07ce78f415990547ba1b4fd5ac8a8_default.png" } } } ] } ``` ### [Processing Strings and Updating Their Status](#processing-strings-and-updating-their-status) [Section titled “Processing Strings and Updating Their Status”](#processing-strings-and-updating-their-status) 1. **App Logic** - The app processes the received strings according to its internal logic (e.g., sending them to an AI service or performing custom validations). 2. [**Updating String Status via API**](#api-methods) - After processing, the app calls Crowdin Enterprise’s private API to update each string’s status on the custom workflow step. This action routes the strings to the appropriate workflow step outputs. #### [API Methods](#api-methods) [Section titled “API Methods”](#api-methods) Below are the API methods for managing string statuses on a custom workflow step. The **Update String Status** method is mandatory, as it finalizes string statuses and routes them to the correct workflow outputs. Another available method **Get Current String Status** is optional, but can help manage edge cases or advanced logic in your app. ### [Configuring the Workflow Step in the Workflow Editor](#configuring-the-workflow-step-in-the-workflow-editor) [Section titled “Configuring the Workflow Step in the Workflow Editor”](#configuring-the-workflow-step-in-the-workflow-editor) Users can configure or delete a custom workflow step in the Crowdin Enterprise workflow editor: * **Updating Settings (`updateSettingsUrl`)** * When a user clicks **Save** after changing step settings in the workflow editor, Crowdin Enterprise sends a **POST** request to the `updateSettingsUrl` defined in the manifest. * The app responds with a **2XX status** to confirm successful handling of the updated configuration. * **Deleting a Step (`deleteSettingsUrl`)** * When a user deletes the step in the workflow editor, Crowdin Enterprise sends a **DELETE** request to the `deleteSettingsUrl`. * The app can safely remove any stored settings related to the deleted workflow step and respond with a **2XX status** to confirm success. ### [Implementing the Settings UI in Your App](#implementing-the-settings-ui-in-your-app) [Section titled “Implementing the Settings UI in Your App”](#implementing-the-settings-ui-in-your-app) If the workflow step provides any settings through the UI, you need to implement validation and saving of the workflow step configuration. #### [Validation Method](#validation-method) [Section titled “Validation Method”](#validation-method) In the iframe UI for your custom workflow step, you need to implement a method to validate the step configuration: ```js window.formRef = { validateForm: () => { // Validate settings form return true; }, } ``` This method is called whenever Crowdin Enterprise checks if the settings are valid before saving. #### [Saving Settings](#saving-settings) [Section titled “Saving Settings”](#saving-settings) To save the workflow step’s configuration, use the following method: ```js window.currentFormData = settings; AP.formDataUpdated(settings); ```
# AI Modules Overview
> Learn about the AI Modules that allow apps to leverage AI capabilities within Crowdin.
AI Modules allow apps to integrate artificial intelligence features, connect to custom AI/MT engines, and extend AI-powered tools within the Crowdin UI. ## [Supported Modules](#supported-modules) [Section titled “Supported Modules”](#supported-modules) | Module | Type | App Scope | Crowdin | Crowdin Enterprise | | --------------------- | ----------------------- | -------------------- | ------- | ------------------ | | Custom AI | `custom-ai` | Account/Organization | ✔ | ✔ | | AI Prompt Provider | `ai-prompt-provider` | Account/Organization | ✔ | ✔ | | AI Request Processors | `ai-request-processors` | Account/Organization | ✔ | ✔ | | AI Tools | `ai-tools` | Project | ✔ | ✔ | | AI Tools Widget | `ai-tools-widget` | Project | ✔ | ✔ | ## [Add Modules to Your Crowdin App](#add-modules-to-your-crowdin-app) [Section titled “Add Modules to Your Crowdin App”](#add-modules-to-your-crowdin-app) To use a module in your app, declare the module in your [App Descriptor](/developer/crowdin-apps-app-descriptor/) file under modules, including any required properties. The properties you include control the customization options for your module. ## [Structure](#structure) [Section titled “Structure”](#structure) manifest.json ```json { "identifier": "application-identifier", "name": "New Cool App", "logo": "/app-logo.png", "baseUrl": "https://app.example.com", "authentication": { "type": "none" }, "scopes": [], "modules": { "{module_type}": [ { "key": "your-module-key", "name": "Module Name" } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | --------------- | --------------------------------------------------------------------------------------------- | | `{module_type}` | **Type:** `string`**Required:** yes**Description:** The type of module Crowdin app uses. | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `name` | **Type:** `string`**Required:** yes**Description:** The human-readable name of the module. |
# File Processing Modules Overview
> Learn about the file processing modules in Crowdin Apps
File processing modules allow apps to add support for new custom file formats and customize processing for supported file formats. ## [Supported Modules](#supported-modules) [Section titled “Supported Modules”](#supported-modules) | Module | Type | App Scope | Crowdin | Crowdin Enterprise | | -------------------------------- | ----------------------------- | -------------------- | ------- | ------------------ | | Custom File Format | `custom-file-format` | Account/Organization | ✔ | ✔ | | File Pre-Import Processing | `file-pre-import` | Account/Organization | ✔ | ✔ | | File Post-Import Processing | `file-post-import` | Account/Organization | ✔ | ✔ | | File Pre-Export Processing | `file-pre-export` | Account/Organization | ✔ | ✔ | | File Post-Export Processing | `file-post-export` | Account/Organization | ✔ | ✔ | | Translation Alignment Processing | `file-translations-alignment` | Account/Organization | ✔ | ✔ | ## [Add Modules to Your Crowdin App](#add-modules-to-your-crowdin-app) [Section titled “Add Modules to Your Crowdin App”](#add-modules-to-your-crowdin-app) To use a module in your app, declare the module in your [App Descriptor](/developer/crowdin-apps-app-descriptor/) file under modules, including any required properties. The properties you include control the customization options for your module. ## [Structure](#structure) [Section titled “Structure”](#structure) manifest.json ```json { "identifier": "application-identifier", "name": "New Cool App", "logo": "/app-logo.png", "baseUrl": "https://app.example.com", "authentication": { "type": "none" }, "scopes": [], "modules": { "{module_type}": [ { "key": "your-module-key", "name": "Module Name" } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | --------------- | --------------------------------------------------------------------------------------------- | | `{module_type}` | **Type:** `string`**Required:** yes**Description:** The type of module Crowdin app uses. | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `name` | **Type:** `string`**Required:** yes**Description:** The human-readable name of the module. |
# UI Modules Overview
> Learn about the UI Modules that allow apps to extend the Crowdin UI
UI Modules allow apps to extend the Crowdin user interface, create integrations with external services, etc. ## [Supported Modules](#supported-modules) [Section titled “Supported Modules”](#supported-modules) | Module | Type | App Scope | Crowdin | Crowdin Enterprise | | ------------------------------------ | ------------------------------- | -------------------- | ------- | ------------------ | | Integrations | `project-integrations` | Project | ✔ | ✔ | | Tools | `project-tools` | Project | ✔ | ✔ | | Reports | `project-reports` | Project | ✔ | ✔ | | Project Menu | `project-menu` | Project | ✔ | ✔ | | Project Menu (Crowdsource View) | `project-menu-crowdsource` | Project | | ✔ | | Editor Right Panel | `editor-right-panel` | Project | ✔ | ✔ | | Editor Asset Panel | `editor-asset-panel` | Project | ✔ | ✔ | | Editor Translations Panel | `editor-translations-panel` | Project | ✔ | ✔ | | Organization Menu | `organization-menu` | Organization | | ✔ | | Organization Menu (Crowdsource View) | `organization-menu-crowdsource` | Organization | | ✔ | | Organization Settings Menu | `organization-settings-menu` | Organization | | ✔ | | Profile Settings Menu | `profile-settings-menu` | Account | ✔ | | | Resources Menu | `profile-resources-menu` | Account | ✔ | | | Custom MT | `custom-mt` | Account/Organization | ✔ | ✔ | | Context Menu | `context-menu` | Configurable | ✔ | ✔ | | Modal | `modal` | Configurable | ✔ | ✔ | ## [Add Modules to Your Crowdin App](#add-modules-to-your-crowdin-app) [Section titled “Add Modules to Your Crowdin App”](#add-modules-to-your-crowdin-app) To use a module in your app, declare the module in your [App Descriptor](/developer/crowdin-apps-app-descriptor/) file under modules, including any required properties. The properties you include control the customization options for your module. ## [Structure](#structure) [Section titled “Structure”](#structure) manifest.json ```json { "identifier": "application-identifier", "name": "New Cool App", "logo": "/app-logo.png", "baseUrl": "https://app.example.com", "authentication": { "type": "none" }, "scopes": [], "modules": { "{module_type}": [ { "key": "your-module-key", "name": "Module Name" } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | --------------- | --------------------------------------------------------------------------------------------- | | `{module_type}` | **Type:** `string`**Required:** yes**Description:** The type of module Crowdin app uses. | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `name` | **Type:** `string`**Required:** yes**Description:** The human-readable name of the module. |
# Crowdin Apps Monetization
> Learn how to monetize your Crowdin Apps
All Crowdin Apps can use one of the following payment models: * **Payment via Crowdin** – Application users can subscribe to the app directly via Crowdin. The platform provides an easy way to make payments. * **Payment via own payment system** – Application users can subscribe directly via the app or other third-party services connected to the app. The developer of the Crowdin App should connect and configure the preferred payment system himself. * **Free usage of the Crowdin Apps** – Application users don’t pay for the app usage. ## [Payment via Crowdin](#payment-via-crowdin) [Section titled “Payment via Crowdin”](#payment-via-crowdin) You can use Crowdin as a payment processor for subscription handling of your apps. To use payment via Crowdin, [Contact Support Team](https://crowdin.com/contacts), so we will create a subscription for your Crowdin App. To implement this payment processor, you need to use an API endpoint to return up-to-date information on the app subscription. You should also add a middleware to your Crowdin App that will limit the access to the app for users that didn’t subscribe. In the middleware, you should make an API request to Crowdin and, depending on the result, implement one of the following actions: * `200 OK` – the subscription is active. Crowdin App must provide access to the functionality by the date specified in the response. This date should be stored within the app to reduce the number of requests to this API endpoint. * `402 Payment Required` – the subscription was not paid. In this case, restrict access to the app functionality and provide the user with a URL to a checkout page. You will get the checkout URL in the response. * `404 Bad Request` – the subscription was not found. It means one of several errors: a user removed a Crowdin App from the organization, or a subscription wasn’t defined for the app. In this case, you should restrict access to the Crowdin App. ### [Request](#request) [Section titled “Request”](#request) To access the Crowdin App subscription API, the app must use the authorization method `crowdin_app` in the [App descriptor](/developer/crowdin-apps-app-descriptor/) and use the received Access Token in the Authorization header. ```bash GET /api/v2/applications/{app-identifier}/subscription ``` **Headers** * Content-type: `application/json` * Authorization: `bearer ` ### [Responses](#responses) [Section titled “Responses”](#responses) **Status code: `200`** ```json { "expires": "2022-12-19 12:00:00" } ``` **Status code: `402`** ```json { "subscribe_link": "https://crowdin.com/checkout?subscribe=..." } ``` **Status code: `400`** ```json { "message": "App identifier not found" } ``` ## [Purchasing a Crowdin App subscription via Crowdin](#purchasing-a-crowdin-app-subscription-via-crowdin) [Section titled “Purchasing a Crowdin App subscription via Crowdin”](#purchasing-a-crowdin-app-subscription-via-crowdin) In the event of subscribing to a Crowdin App, a user will be directed to a checkout page with the app subscription details. Please note that the first payment amount might differ from the app subscription cost. The subscription cost of the app is proportionally prorated over a billing cycle, and, e.g., if a user subscribes to an app at the beginning of his billing cycle, he will pay the app’s subscription price in full. And if a user subscribes to an app in the middle of his billing cycle, he will pay only half of the app’s subscription price. * Crowdin Checkout page  * Crowdin Enterprise Checkout page  Let’s review the possible scenario below: The total subscription price for a Crowdin App is $30/month. Suppose today is the 1st day of the month, and the next billing cycle starts on the 10th of the current month, so when subscribing to the app, a user will need to pay $10. ```md $30 / 30 days (billing cycle) * 10 days (prorate period) = $10 ``` On the next billing cycle, the app subscription will be included in the user’s Crowdin subscription in full.
# Crowdin Apps Promoting
> Learn how to promote your Crowdin App
Creating a Crowdin App and publishing it in the Crowdin Store is only half of the application’s success. An equally important part is the promotion of your application. The methods described in the article don’t claim to be exhaustive but rather give you a good starting point. ## [Build Landing Page and Help Docs](#build-landing-page-and-help-docs) [Section titled “Build Landing Page and Help Docs”](#build-landing-page-and-help-docs) Creating a landing page and help docs increase the conversion rate for your Crowdin App and improve the application’s credibility. It will also help to promote the application in other ways. ## [Write a Blog Post](#write-a-blog-post) [Section titled “Write a Blog Post”](#write-a-blog-post) The blog post is an excellent opportunity to show off your Crowdin App in more detail. Describe how your Crowdin App works and focus your post on the benefits for your customers. Describe why users should use your application and show examples of workflows or customer success stories. ## [Promote on Social Media](#promote-on-social-media) [Section titled “Promote on Social Media”](#promote-on-social-media) Social networking is a great way to spread the news. Make your posts concise, show the benefits and new updates of your Crowdin App, and attach images or GIFs that show your application in action. ## [Message Your Customers](#message-your-customers) [Section titled “Message Your Customers”](#message-your-customers) Targeted messaging via emails or in-app messages - is a great way to get people to use your app right away. Focus on messaging customers who are likely to get value from your app or whom you know are Crowdin users.
# Publishing Your App
> Learn how to publish your app in the Crowdin Store
When you’re ready to share your app, you can submit your app to the [Crowdin Store](https://store.crowdin.com). Once published, your app will become accessible for installation and usage by other Crowdin users. But before publishing an app, you need to obtain an author account in the Crowdin Store. To do that, [contact our customer success service](https://crowdin.com/contacts) and follow the provided instructions. ## [Creating Author Page](#creating-author-page) [Section titled “Creating Author Page”](#creating-author-page) Every developer that publishes apps in the Crowdin Store will have a personal author page. Here is an example of the author page: [Crowdin](https://store.crowdin.com/author/5). Once you obtain your author account, you can proceed to your author page creation. To do that, navigate to the [Authors management page](https://developer.app.crowdin.net/admin/content/author) and click **Create Item**. You’ll need to provide some information for your author page. We recommend doing this before you start the publishing process. In the table below, you can check out the fields that need to be filled for your author page. | Field | Description | Required | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | | Status | The publishing status of the page. Leave it as a **Draft** until you fill in all the required information and are ready to publish it. | yes | | Logo | The Author’s logo. We recommend using a .png image with a minimum resolution of 256×256 pixels with a transparent background. | yes | | Description | Add a brief description of up to 200 characters. | yes | | Support URL | This can be a link to a support page. | yes | | Email | Author’s email. | yes | | Name | Author’s display name. | yes | | URL | This can be a link to a business website. | no | | Verification | Defines whether the author is verified by the Crowdin Team. Adds the **Verified author** badge to the Author’s page. Can be set exclusively by the Crowdin Team. | no | ## [Preparing App Page](#preparing-app-page) [Section titled “Preparing App Page”](#preparing-app-page) Every app published in the Crowdin Store will have its own app page. This allows other Crowdin users to find and install your app. To start creating an app page, navigate to the [Item management page](https://developer.app.crowdin.net/admin/content/Item) and click **Create Item**. You’ll need to provide some information for your app page. We recommend doing this before you start the publishing process. In the table below, you can check out the fields that need to be filled for your app page. | Field | Description | Required | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | | Status | The publishing status of the page. Leave it as a **Draft** until you fill in all the required information and are ready to publish the app. | yes | | Beta | Defines the app’s lifecycle status. A beta phase typically begins when the app is feature-complete but likely to contain several known or unknown bugs. Adds a corresponding label on the app’s page. | no | | Name | Specify a name for your app. This can be something descriptive or a bit creative. Users can search for your app using this name, but you also have tags to add relevant keywords. | yes | | Slug | The unique app identifier within Crowdin Store. Ensure your app slug matches the app identifier in the app’s `manifest.json` file. You can use up to 15 alphanumeric characters in your slug, including hyphens `-`. | yes | | Logo | Upload an image to represent your app in the Crowdin Store. Crowdin will use this image to identify your app in the Store. Ensure the logo would fit nicely into square and circle shapes in the product UI. Recommended size: 400×400 pixels. Also, the .png format with a transparent background is preferred. | yes | | Tagline | The tweet-sized app description. Make it an attention grabber. | yes | | Content | The app description goes here. Describe how the app works and how Crowdin users can benefit from it. Use between 80-240 words. Markdown syntax is supported. Also, you can add screenshots of your app. | yes | | Tags | Tags allow to group and filter apps by keywords. | yes | | Author | The app’s author. Select the [author created in a previous step](#creating-author-page). | no | | Product | The product compatibility. Defines whether the app is compatible with the specific product. Select carefully. Multiple choices are allowed. | yes | | Type | The app type. See the [complete app type list](#app-types). | yes | | Category | Select the categories your app best fits in. You can select multiple categories. | yes | | Manifest | The URL to the app’s manifest.json file. It’s required only for the Application and File app types. | in some cases | | Manifest Identifier | Specify your app manifest identifier if your slug is different. | in some cases | | URL | The additional external link to resources related to your app. It’s required for the Guide items. Optionally, it can be specified for any other kinds of items. | in some cases | | URL Enterprise | Similar to the URL field. The main difference is that this URL will be displayed in the Crowdin Enterprise Store. | in some cases | | Resources | Allows adding Resource URLs and titles and displaying them in the application. | no | | Target Blank | A special kind of item in the Crowdin Store that has no own page. It’s just an external link. If enabled, URL or/and URL Enterprise are required. | no | | Sort | The score for sorting by Popularity. A higher value means higher popularity. | no | | Pricing | Describes the app’s pricing model in JSON format. See [App Pricing](#app-pricing) to check out the possible configurations. | yes | | Code | Required only for the QA Check items. Fill it in with your QA Check code. | in some cases | | Config | Show installation wizard. | no | | Meta Title | The meta title that will be used during a page rendering. | no | | Meta Description | The meta description for the app’s page. | no | | CTA Section | Enables the CTA section at the bottom of the item description. | no | ### [App Types](#app-types) [Section titled “App Types”](#app-types) The app type list: | Type | Description | | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Application | A regular Crowdin Application (e.g., Project Integration, Editor Panel, Resources or Reports app). Should have Manifest. | | Guide | The item that describes some integration or functionality. Usually, it has an external link. | | QA Check | The custom QA Check. Only available in Crowdin Enterprise. To see the examples, visit the [QA Check section](https://store.crowdin.com/types/qa-check) in the Crowdin Store. | | File | The custom file format. Should have Manifest. | | System | The native Crowdin Integration or file extension. | ### [App Description](#app-description) [Section titled “App Description”](#app-description) The **Content** field is crucial for helping users understand your app’s value. Here are some tips for creating compelling app descriptions that convert visitors into users. #### [Structure Your Content](#structure-your-content) [Section titled “Structure Your Content”](#structure-your-content) A well-structured description helps users quickly understand what your app does: 1. **Lead with value** - Start with the main benefit your app provides 2. **Explain how it works** - Describe the key features and workflow 3. **Include use cases** - Help users envision how they’d use your app 4. **Add visuals** - Screenshots make your app more tangible #### [Using Admonitions](#using-admonitions) [Section titled “Using Admonitions”](#using-admonitions) You can use admonitions to highlight important information in your app description. The following admonition types are supported: ```md :::note Use notes to provide additional context or clarify important details about your app's functionality. ::: :::tip[Pro Tip] Tips are great for sharing best practices or suggesting optimal ways to use your app. ::: :::info Use info blocks to highlight key features or compatibility information. ::: :::warning Warnings help users understand limitations, prerequisites, or potential issues they should be aware of before installation. ::: :::danger Use danger admonitions sparingly to highlight critical information, such as actions that cannot be undone or potential data loss scenarios. ::: ``` ### [App Pricing](#app-pricing) [Section titled “App Pricing”](#app-pricing) The pricing field describes the app’s pricing model in JSON format. It is used only for rendering pricing info on an app page. You should also implement the behavior in your app. Read more about [Crowdin Apps Monetization](/developer/crowdin-apps-monetization/). If your app is free, just leave the empty array `[]` in the pricing field. If your app is paid, click **Fill with Template value** and adjust the pricing description. Example: ```json [ { "plan_type": "recurring", "price": { "monthly": "80", "yearly": "800", "currency": "USD" }, "trial": true } ] ``` Where: | Field | Description | | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `plan_type` | The type of app’s plan. Possible values: `recurring` and `free`. | | `price` | The price of the app. Should contain an object with the `monthly` or/and `yearly` value. Available `currency` values are “USD” and “EUR”. | | `trial` | Defines if the application has a trial period. By default it’s 14 days for crowdin.com and 30 days for Crowdin Enterprise. | ## [App Review Process](#app-review-process) [Section titled “App Review Process”](#app-review-process) The application needs to go through the review process before Crowdin can list it in the Crowdin Store. The review process includes reviewing the author’s and app’s descriptions on the respective pages in the Crowdin Store. ### [Author Verification](#author-verification) [Section titled “Author Verification”](#author-verification) You can request author verification for your organization. A **Verified author** badge is a check that appears next to the author account name in the Crowdin Store and means Crowdin has confirmed that an author account is the authentic presence of the company or individual it represents. To request an author verification, [contact our customer success service](https://crowdin.com/contacts), and we will agree on the further steps.
# Quick Start
> Learn how to build a Crowdin Application using Node.js
In this article, you’ll learn the basic principles of building a Crowdin Application using Node.js and the Vercel platform. You’ll build and deploy a sample app using Next.js along the way. **Prerequisites**: * Installed [Node.js (version 18 or later)](https://nodejs.org/en/download/) and npm or pnpm. * Registered account on [Vercel](https://vercel.com/) with access to GitHub or another Git provider. * Crowdin account with permissions to create and install apps. * Created [OAuth application](/developer/authorizing-oauth-apps/) in Crowdin with `Client ID` and `Client Secret` values. These credentials will be used for authentication. ## [Setup](#setup) [Section titled “Setup”](#setup) In this step, download the sample app to your local machine and set up your development environment. Clone the repository: ```bash git clone https://github.com/crowdin/apps-quick-start-nextjs.git cd apps-quick-start-nextjs git checkout v1.0-basic ``` Install the required dependencies: * npm ```bash npm install ``` * pnpm ```bash pnpm install ``` Copy the example environment file: ```bash cp .env.example .env.local ``` Open the `.env.local` file and update it with your app credentials: .env.local ```ini # Where your app runs locally NEXT_PUBLIC_BASE_URL=http://localhost:3000 # Credentials from Crowdin OAuth app CROWDIN_CLIENT_ID= CROWDIN_CLIENT_SECRET= # Crowdin OAuth endpoint AUTH_URL=https://accounts.crowdin.com/oauth/token # Crowdin Apps iframe script (CDN) NEXT_PUBLIC_CROWDIN_IFRAME_SRC=https://cdn.crowdin.com/apps/dist/iframe.js ``` Start the development server: * npm ```bash npm run dev ``` * pnpm ```bash pnpm dev ``` Once the app is running, open `http://localhost:3000` in your browser. You should see the app welcome page. At this point, you have a working app with the following structure: * `app/manifest.json/route.ts` – Serves the app manifest dynamically. * `app/project-menu/page.tsx` – The Project Menu module loaded inside Crowdin. In the current state, the app includes only the **Project Menu** module and does not yet require authentication. You’ll add OAuth and custom file format support in the next steps. ## [App Manifest](#app-manifest) [Section titled “App Manifest”](#app-manifest) In this step, you’ll review the app manifest that describes your Crowdin App and defines how it integrates with the Crowdin interface. The manifest is served dynamically using a dedicated route located at `app/manifest.json/route.ts`. This file returns the required metadata about your app. app/manifest.json/route.ts ```ts import { NextResponse } from "next/server"; export async function GET() { const manifestData = { identifier: "getting-started", name: "Getting Started", baseUrl: process.env.NEXT_PUBLIC_BASE_URL, logo: "/logo.svg", authentication: { type: "none" }, scopes: ["project"], modules: { "project-menu": [ { key: "menu", name: "Getting Started", url: "/project-menu" } ] }, }; return NextResponse.json(manifestData); } ``` ### [Manifest Highlights](#manifest-highlights) [Section titled “Manifest Highlights”](#manifest-highlights) * **baseUrl** – The root domain where your app is deployed. Crowdin uses this value to construct URLs for iframe modules and API calls. When deployed to Vercel, the production domain is injected automatically. * **authentication** – Set to `"none"` for this basic version of the app. We’ll change this to enable OAuth authentication later. * **modules**: * `project-menu` – Adds a new tab inside Crowdin projects. When clicked, it opens the `/project-menu` route of your app inside an iframe. Once the app is deployed, the manifest will be available at the following URL: ```plaintext https://.vercel.app/manifest.json ``` You’ll use this URL in the **Install from URL** dialog when installing the app in your Crowdin account later in this guide. ## [Deploying the App](#deploying-the-app) [Section titled “Deploying the App”](#deploying-the-app) In this step, you’ll deploy the app to the Vercel platform and obtain the production URL that will be used as the app’s `baseUrl`. To deploy the app, follow these steps: 1. Push the app code to a GitHub repository. 2. Log in to [Vercel](https://vercel.com/) and select **Import Git Repository**. 3. Choose your repository and proceed with the setup. 4. In the **Environment Variables** section, add the variables from your `.env.local` file: * `CROWDIN_CLIENT_ID` * `CROWDIN_CLIENT_SECRET` * `NEXT_PUBLIC_BASE_URL` – set to your future production URL, e.g., `https://.vercel.app` * `AUTH_URL` – `https://accounts.crowdin.com/oauth/token` * `NEXT_PUBLIC_CROWDIN_IFRAME_SRC` – `https://cdn.crowdin.com/apps/dist/iframe.js` 5. Click **Deploy**. Once deployed, Vercel will assign a production URL to your app. This URL will be used as the `baseUrl` in your manifest, as described in the previous step. You’ll use it shortly to install the app in your Crowdin account. ## [Installing the App in Crowdin](#installing-the-app-in-crowdin) [Section titled “Installing the App in Crowdin”](#installing-the-app-in-crowdin) Once your app is deployed, you can install it in your Crowdin account using the [manual installation](/developer/crowdin-apps-installation/) method. Use the production manifest URL from your deployed Vercel app, for example: ```plaintext https://.vercel.app/manifest.json ``` After installation, a new tab called **Getting Started** will appear in your project navigation. If the app welcome page opens successfully, the app was installed correctly. ## [Adding API Access](#adding-api-access) [Section titled “Adding API Access”](#adding-api-access) This section is optional and applies if you want your app to access the Crowdin API on behalf of the user or organization. ### [Setting Up the Database with Prisma](#setting-up-the-database-with-prisma) [Section titled “Setting Up the Database with Prisma”](#setting-up-the-database-with-prisma) To securely store organization credentials received during app installation, you’ll need a database. This step uses [Prisma](https://www.prisma.io/) as an ORM. You can use SQLite for local development or switch to PostgreSQL or another provider for production. The data model is defined in `prisma/schema.prisma` and includes a single `Organization` model: prisma/schema.prisma ```prisma datasource db { provider = "postgresql" url = env("DATABASE_URL") } generator client { provider = "prisma-client-js" } model Organization { id String @id @default(cuid()) domain String? organizationId Int userId Int baseUrl String appId String appSecret String accessToken String accessTokenExpires Int createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("organizations") } ``` Add the database connection string to your environment variables: .env.local ```ini # Database connection (PostgreSQL) DATABASE_URL="postgresql://username:password@localhost:5432/crowdin_app_db" ``` If your app is already deployed to Vercel, update the environment variables in your Vercel dashboard and redeploy. To apply the schema and generate the local database, run the following command: ```bash npx prisma migrate dev --name init ``` This command creates a local SQLite database (or another provider if configured) and generates the required Prisma client. At this point, your app is ready to store and retrieve installation data. In the next step, you’ll configure routes to handle `installed` and `uninstall` events from Crowdin. ### [Handling Install and Uninstall Events](#handling-install-and-uninstall-events) [Section titled “Handling Install and Uninstall Events”](#handling-install-and-uninstall-events) When a Crowdin App is installed or uninstalled, Crowdin sends a signed `POST` request to the app’s backend. You’ll now create a dynamic route that handles both events. The handler is located in `app/events/[slug]/route.ts`. Based on the route parameter, it processes either the `installed` or `uninstall` event: app/events/\[slug]/route.ts ```ts import { NextResponse } from 'next/server'; import { prisma } from '@/lib/prisma'; import { refreshCrowdinToken } from '@/lib/crowdinAuth'; /** Data structure received when Crowdin fires the *installed* event. */ interface InstalledBody { appId: string; appSecret: string; domain: string; organizationId: string | number; userId: string | number; baseUrl: string; } /** Data structure received when Crowdin fires the *uninstall* event. */ interface UninstallBody { domain: string; organizationId: string | number; } /** * Unified POST handler for Crowdin *App events* (`installed`, `uninstall`). * Dispatches based on the dynamic `slug` in the route. */ export async function POST(request: Request, { params }: { params: Promise<{ slug: string }> }) { const body = await request.json(); const { slug } = await params; switch (slug) { case 'installed': { const { CROWDIN_CLIENT_ID, CROWDIN_CLIENT_SECRET, AUTH_URL } = process.env; if (!CROWDIN_CLIENT_ID || !CROWDIN_CLIENT_SECRET || !AUTH_URL) { console.error('Missing environment variables for Crowdin OAuth'); return NextResponse.json({ error: 'Server configuration error' }, { status: 500 }); } const eventBody = body as InstalledBody; let newTokenData: { accessToken: string; accessTokenExpires: number }; try { newTokenData = await refreshCrowdinToken({ appId: eventBody.appId, appSecret: eventBody.appSecret, domain: eventBody.domain, userId: Number(eventBody.userId), }); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Failed to obtain Crowdin token during installation.'; return NextResponse.json({ error: errorMessage }, { status: 500 }); } const organizationData = { domain: eventBody.domain, organizationId: Number(eventBody.organizationId), appId: eventBody.appId, appSecret: eventBody.appSecret, userId: Number(eventBody.userId), baseUrl: eventBody.baseUrl, accessToken: newTokenData.accessToken, accessTokenExpires: newTokenData.accessTokenExpires, }; try { const existingOrganization = await prisma.organization.findFirst({ where: { domain: eventBody.domain, organizationId: Number(eventBody.organizationId), }, }); if (existingOrganization) { await prisma.organization.update({ where: { id: existingOrganization.id }, data: organizationData, }); } else { await prisma.organization.create({ data: organizationData, }); } return NextResponse.json( { message: 'Installation processed successfully' }, { status: 200 } ); } catch (dbError) { console.error('Database error during installed event:', dbError); return NextResponse.json({ error: 'Database operation failed' }, { status: 500 }); } } case 'uninstall': { const eventBody = body as UninstallBody; try { await prisma.organization.deleteMany({ where: { domain: eventBody.domain, organizationId: Number(eventBody.organizationId), }, }); return NextResponse.json( { message: 'Uninstallation processed successfully' }, { status: 200 } ); } catch (dbError) { console.error('Database error during uninstall event:', dbError); return NextResponse.json({ error: 'Database operation failed' }, { status: 500 }); } } default: return NextResponse.json({ error: 'Not found' }, { status: 404 }); } } ``` This logic does the following: * On `installed`, saves the organization and app credentials to the database. * On `uninstall`, removes the organization entry. To activate these handlers, update your app manifest by adding the `events` block and changing the authentication type: app/manifest.json/route.ts ```ts import { NextResponse } from 'next/server'; export async function GET() { const manifestData = { identifier: 'getting-started', name: 'Getting Started', baseUrl: process.env.NEXT_PUBLIC_BASE_URL, logo: '/logo.svg', authentication: { type: 'crowdin_app', clientId: process.env.CROWDIN_CLIENT_ID, }, events: { installed: '/events/installed', uninstall: '/events/uninstall', }, scopes: ['project'], modules: { 'project-menu': [{ key: 'menu', name: 'Getting Started', url: '/project-menu' }] }, }; return NextResponse.json(manifestData); } ``` After these changes, Crowdin will call the specified routes during app installation and removal. ### [Adding JWT Middleware](#adding-jwt-middleware) [Section titled “Adding JWT Middleware”](#adding-jwt-middleware) When a Crowdin App is opened inside a project, Crowdin includes a signed JWT token in the request. To verify the token and extract user context, you’ll add middleware to your app. Create the `middleware.ts` file in the root of your project and add the following code: middleware.ts ```ts import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; import { jwtVerify } from 'jose'; interface DecodedJwtPayload { domain: string; context: { organization_id: number; user_id: number; }; iat?: number; exp?: number; } const CROWDIN_CLIENT_SECRET = process.env.CROWDIN_CLIENT_SECRET; export async function middleware(request: NextRequest) { if (!request.nextUrl.pathname.startsWith('/api')) { return NextResponse.next(); } const authHeader = request.headers.get('Authorization'); let token: string | undefined | null = authHeader?.startsWith('Bearer ') ? authHeader.split(' ')[1] : undefined; if (!token) { token = request.nextUrl.searchParams.get('jwtToken'); } if (!token) { return NextResponse.json( { error: { message: 'User is not authorized. Missing or invalid token.' } }, { status: 401 } ); } if (!CROWDIN_CLIENT_SECRET) { console.error('CROWDIN_CLIENT_SECRET is not defined in environment variables for middleware.'); return NextResponse.json( { error: { message: 'Server configuration error in middleware.' } }, { status: 500 } ); } try { const secretKey = new TextEncoder().encode(CROWDIN_CLIENT_SECRET); const { payload } = (await jwtVerify(token, secretKey)) as { payload: DecodedJwtPayload }; const decodedJwt = payload; console.log('decodedJwt', decodedJwt); if (!decodedJwt.context?.user_id || !decodedJwt.context?.organization_id) { console.error('Middleware: JWT is missing necessary fields (user_id or organization_id).'); return NextResponse.json({ error: { message: 'Invalid token payload.' } }, { status: 403 }); } const requestHeaders = new Headers(request.headers); if (decodedJwt) { requestHeaders.set('x-decoded-jwt', JSON.stringify(decodedJwt)); } return NextResponse.next({ request: { headers: requestHeaders, }, }); } catch (error) { console.error('Middleware JWT verification failed:', error); let errorMessage = 'User is not authorized. Token verification failed.'; if ( error instanceof Error && (error.name === 'JWTExpired' || error.name === 'JWSSignatureVerificationFailed' || error.name === 'JWSInvalid') ) { errorMessage = `Token error: ${error.message}`; } return NextResponse.json({ error: { message: errorMessage } }, { status: 403 }); } } ``` Next.js will automatically run this middleware for every request that matches a path defined in the `matcher` configuration. Define the matcher at the end of the file: middleware.ts ```ts export const config = { matcher: ['/api/user/:path*'], }; ``` This ensures that any sensitive route (such as user info or file processing) is only accessible if the token is present and valid. ### [Creating the /api/user Route](#creating-the-apiuser-route) [Section titled “Creating the /api/user Route”](#creating-the-apiuser-route) You’ll now create a protected API route that returns information about the currently authenticated Crowdin user. This route uses the decoded JWT payload and the stored organization credentials to retrieve a valid access token and make an API request to Crowdin. Create the `app/api/user/route.ts` file and add the following code: app/api/user/route.ts ```ts import { NextResponse, NextRequest } from 'next/server'; import { prisma } from '@/lib/prisma'; import CrowdinApiClient from '@crowdin/crowdin-api-client'; import { getValidOrganizationToken } from '@/lib/crowdinAuth'; /** * Subset of the JWT payload we expect from Crowdin. Provided by a middleware * that decodes and verifies the token before reaching this handler. */ interface DecodedJwtPayload { domain: string; context: { organization_id: number; }; iat?: number; exp?: number; } /** * Extract organisation sub-domain (if any) from a Crowdin `baseUrl`. */ function getOrganizationDomain(baseUrl: string): string | undefined { try { const url = new URL(baseUrl); if (url.hostname.endsWith('.crowdin.com')) { return url.hostname.split('.')[0]; } } catch (error) { console.error('Invalid baseUrl format:', baseUrl, error); } return undefined; } /** * Handle `GET /api/user` request – fetch the authenticated Crowdin user via * Crowdin API. Requires a valid JWT (decoded by middleware) in the * `x-decoded-jwt` header. */ export async function GET(request: NextRequest) { const decodedJwtString = request.headers.get('x-decoded-jwt'); if (!decodedJwtString) { console.error('Decoded JWT not found in headers. Middleware might not have run or failed.'); return NextResponse.json( { error: { message: 'Authentication data not found.' } }, { status: 500 } ); } let decodedJwt: DecodedJwtPayload; try { decodedJwt = JSON.parse(decodedJwtString) as DecodedJwtPayload; } catch (error) { console.error('Failed to parse decoded JWT from headers:', error); return NextResponse.json( { error: { message: 'Invalid authentication data format.' } }, { status: 500 } ); } const organizationFromDb = await prisma.organization.findFirst({ where: { domain: decodedJwt.domain, organizationId: Number(decodedJwt.context.organization_id), }, }); if (!organizationFromDb) { return NextResponse.json({ error: { message: 'Organization not found.' } }, { status: 404 }); } try { const validAccessToken = await getValidOrganizationToken(organizationFromDb.id); const organizationDomain = getOrganizationDomain(organizationFromDb.baseUrl); const crowdinClient = new CrowdinApiClient({ token: validAccessToken, ...(organizationDomain && { organization: organizationDomain }), }); const userResponse = await crowdinClient.usersApi.getAuthenticatedUser(); return NextResponse.json(userResponse.data || {}, { status: 200 }); } catch (error: unknown) { console.error('Error in GET /api/user:', error); let errorMessage = 'An unknown error occurred.'; let statusCode = 500; if (error instanceof Error) { errorMessage = error.message; if ( errorMessage.includes('Organization not found') || errorMessage.includes('Failed to refresh Crowdin token') ) { statusCode = 400; } } return NextResponse.json({ error: { message: errorMessage } }, { status: statusCode }); } } ``` This route performs the following: * Reads the decoded JWT payload from the request headers * Locates the organization by `domain` and `organizationId` * Retrieves or refreshes the access token using a helper function * Instantiates the Crowdin API client * Returns the current user’s information as JSON Make sure the `/api/user` route is included in your middleware matcher so it’s protected by the JWT verification logic: middleware.ts ```ts export const config = { matcher: ['/api/user/:path*'], }; ``` You can now test the integration by opening your installed app in Crowdin and calling the `/api/user` route, for example by clicking a **Show User Details** button in your Project Menu module. ## [Supporting a Custom File Format](#supporting-a-custom-file-format) [Section titled “Supporting a Custom File Format”](#supporting-a-custom-file-format) This section is optional and applies if you want your app to process custom files uploaded to Crowdin. You’ll configure the `custom-file-format` module in your manifest, define the processing route, and handle file parsing and preview generation on the backend. By the end of this section, your app will: * Detect and process `.json` files that contain a specific key (e.g. `"hello_world"`) * Extract source strings and provide an HTML preview for translators * Rebuild translated files for export from Crowdin To implement this, follow the steps below. ### [Defining the Module in the Manifest](#defining-the-module-in-the-manifest) [Section titled “Defining the Module in the Manifest”](#defining-the-module-in-the-manifest) To support processing custom files in Crowdin, define a `custom-file-format` module in your app manifest. In this example, the app will handle `.json` files that contain the `"hello_world"` key. app/manifest.json/route.ts ```ts import { NextResponse } from 'next/server'; export async function GET() { const manifestData = { identifier: 'getting-started', name: 'Getting Started', baseUrl: process.env.NEXT_PUBLIC_BASE_URL, logo: '/logo.svg', authentication: { type: 'crowdin_app', clientId: process.env.CROWDIN_CLIENT_ID, }, events: { installed: '/events/installed', uninstall: '/events/uninstall', }, scopes: ['project'], modules: { 'project-menu': [ { key: 'menu', name: 'Getting Started', url: '/project-menu', }, ], 'custom-file-format': [ { key: 'custom-file-format', type: 'custom-file-format', url: '/api/file/process', signaturePatterns: { fileName: '.+\\.json$', fileContent: '"hello_world":', }, }, ], }, }; return NextResponse.json(manifestData); } ``` This configuration tells Crowdin: * To send matching files to your app’s `/api/file/process` route * To trigger this module only for `.json` files that include the key `"hello_world"` Crowdin will send the file contents to your app when parsing or rebuilding the file during the import/export flow. ### [Creating the File Processing Route](#creating-the-file-processing-route) [Section titled “Creating the File Processing Route”](#creating-the-file-processing-route) To handle file parsing and rebuilding, you’ll create a backend route that responds to Crowdin’s `POST` requests. This route will distinguish between two job types: `parse-file` and `build-file`. Create the following route file: app/api/file/process/route.ts ```ts import { NextResponse, NextRequest } from 'next/server'; import { parseFile, buildFile } from '@/lib/fileProcessing'; import { TranslationEntry } from '@/lib/file-utils/types'; /** * Supported job types for the file processing endpoint. */ type JobType = 'parse-file' | 'build-file'; /** * Request body definition expected by the `/api/file/process` endpoint. */ interface ProcessRequestBody { jobType: JobType | unknown; file: { content?: string; contentUrl?: string; name: string }; targetLanguages: { id: string }[]; strings?: TranslationEntry[]; stringsUrl?: string; } const validateCommonFields = (body: ProcessRequestBody): { isValid: boolean; error?: string } => { if (!body.file) { return { isValid: false, error: 'File is missing in request' }; } if (!body.file.name) { return { isValid: false, error: 'File name is missing' }; } if (!(body.file.content || body.file.contentUrl)) { return { isValid: false, error: 'File content or URL is missing' }; } return { isValid: true }; }; const validateBuildFileRequest = ( body: ProcessRequestBody ): { isValid: boolean; error?: string } => { if (!(body.strings || body.stringsUrl)) { return { isValid: false, error: 'For build-file, you need to provide strings or stringsUrl' }; } return { isValid: true }; }; const handleParseFile = async (body: ProcessRequestBody) => { const validation = validateCommonFields(body); if (!validation.isValid) { return NextResponse.json({ error: { message: validation.error } }, { status: 400 }); } const response = await parseFile({ file: body.file, targetLanguages: body.targetLanguages, }); return NextResponse.json(response, { status: 200, headers: { 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Content-Type': 'application/json', }, }); }; const handleBuildFile = async (body: ProcessRequestBody) => { const commonValidation = validateCommonFields(body); if (!commonValidation.isValid) { return NextResponse.json({ error: { message: commonValidation.error } }, { status: 400 }); } const buildValidation = validateBuildFileRequest(body); if (!buildValidation.isValid) { return NextResponse.json({ error: { message: buildValidation.error } }, { status: 400 }); } // Create proper request object with correct types const buildRequest: { file: { content?: string; contentUrl?: string; name: string }; targetLanguages: { id: string }[]; strings?: TranslationEntry[]; stringsUrl?: string; } = { file: body.file, targetLanguages: body.targetLanguages, }; // Only add strings if it exists if (body.strings) { buildRequest.strings = body.strings; } // Only add stringsUrl if it exists if (body.stringsUrl) { buildRequest.stringsUrl = body.stringsUrl; } const response = await buildFile(buildRequest); return NextResponse.json(response, { status: 200, headers: { 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Content-Type': 'application/json', }, }); }; /** * Primary entry point – decide which file operation to perform based on * `jobType` and delegate to the corresponding handler. */ export async function POST(request: NextRequest) { try { const body = (await request.json()) as ProcessRequestBody; if (!body.jobType) { return NextResponse.json( { error: { message: 'Missing jobType parameter in request' } }, { status: 400 } ); } switch (body.jobType) { case 'parse-file': return await handleParseFile(body); case 'build-file': return await handleBuildFile(body); default: const jobTypeMessage = typeof body.jobType === 'string' ? body.jobType : 'unknown type'; return NextResponse.json( { error: { message: `Unknown job type: ${jobTypeMessage}` } }, { status: 400 } ); } } catch (e: unknown) { console.error('Error processing file:', e); const errorMessage = e instanceof Error ? e.message : 'An unknown error occurred while processing the file'; return NextResponse.json( { error: { message: errorMessage, stack: process.env.NODE_ENV === 'development' && e instanceof Error ? e.stack : undefined, }, }, { status: 500, headers: { 'Content-Type': 'application/json', }, } ); } } ``` This route: * Receives payloads from Crowdin when a file is uploaded or exported * For `parse-file`, extracts source strings and builds a preview * For `build-file`, injects translations into the original structure Make sure the `/api/file/process/:path*` route is included in your middleware matcher so it’s protected by the JWT verification logic: middleware.ts ```ts export const config = { matcher: ['/api/user/:path*', '/api/file/process/:path*'], }; ``` In the next step, you’ll implement the logic behind `parseFile` and `buildFile` in a helper module. ### [Implementing File Parsing and Rebuilding](#implementing-file-parsing-and-rebuilding) [Section titled “Implementing File Parsing and Rebuilding”](#implementing-file-parsing-and-rebuilding) Now you’ll implement the logic behind the `parseFile` and `buildFile` functions referenced in the `/api/file/process` route. These helpers extract strings from uploaded files, generate a preview, and rebuild translated files during export. Create the following helper file: lib/fileProcessing.ts ```ts 'use server'; import React from 'react'; import FilePreview from './FilePreview'; import { TranslationEntry, ParseFileRequest, BuildFileRequest, PreviewStrings, } from './file-utils/types'; import { uploadToBlob, exceedsMaxSize, generateUniqueFileName } from './file-utils/blob-storage'; import { getContent, getStringsForExport, getTranslation } from './file-utils/content-processor'; /** * Processes the input file and generates strings for translation * @param req The request to analyze the file * @returns Strings for translation and HTML preview */ export async function parseFile(req: ParseFileRequest) { const fileContent = await getContent(req.file); const hasTargetLanguage = req.targetLanguages?.[0]?.id != null; const { sourceStrings, previewStrings } = extractStringsFromContent( fileContent, hasTargetLanguage && req.targetLanguages[0] ? req.targetLanguages[0].id : undefined ); const previewHtml = await generatePreviewHtml(req.file.name || 'Unknown file', previewStrings); const fileBaseName = generateUniqueFileName(req.file.name); const serializedStrings = JSON.stringify(sourceStrings); if (!exceedsMaxSize(serializedStrings)) { return { data: { strings: sourceStrings, preview: Buffer.from(previewHtml).toString('base64'), }, }; } return { data: { stringsUrl: await uploadToBlob( serializedStrings, `parsed_files/${fileBaseName}_strings.json`, 'application/json' ), previewUrl: await uploadToBlob( previewHtml, `parsed_files/${fileBaseName}_preview.html`, 'text/html' ), }, }; } /** * Creates a file with translated strings * @param req The request to create a file * @returns File content or URL to download */ export async function buildFile(req: BuildFileRequest) { const languageId = req.targetLanguages?.[0]?.id; if (!languageId) { throw new Error('Target language ID is missing'); } const fileContent = await getContent(req.file); const translations = await getStringsForExport(req); if (!fileContent || typeof fileContent !== 'object' || Object.keys(fileContent).length === 0) { throw new Error('No content to translate or invalid file content format'); } const translatedContent = translateFileContent(fileContent, translations, languageId); const responseContent = JSON.stringify(translatedContent, null, 2); const fileBaseName = generateUniqueFileName(req.file.name); if (!exceedsMaxSize(responseContent)) { return { data: { content: Buffer.from(responseContent).toString('base64'), }, }; } return { data: { contentUrl: await uploadToBlob( responseContent, `built_files/${fileBaseName}_content.json`, 'application/json' ), }, }; } /** * Extracts strings for translation from the file content * @param fileContent The file content * @param languageId The language ID (optional) * @returns Object with strings for translation and preview */ function extractStringsFromContent( fileContent: Record, languageId?: string ): { sourceStrings: TranslationEntry[]; previewStrings: PreviewStrings } { const sourceStrings: TranslationEntry[] = []; const previewStrings: PreviewStrings = {}; let previewIndex = 0; if (!fileContent || typeof fileContent !== 'object') { return { sourceStrings, previewStrings }; } for (const key in fileContent) { const value = fileContent[key]; if (typeof value !== 'string') { continue; } let entryTranslations: Record = {}; if (languageId) { entryTranslations = { [languageId]: { text: value } }; } sourceStrings.push({ identifier: key, context: `Some context: \n ${value}`, customData: '', previewId: previewIndex, labels: [], isHidden: false, text: value, translations: entryTranslations, }); previewStrings[key] = { text: value, id: previewIndex, }; previewIndex++; } return { sourceStrings, previewStrings }; } /** * Generates HTML preview for the file * @param fileName The file name * @param previewStrings Strings for preview * @returns HTML code for preview */ async function generatePreviewHtml( fileName: string, previewStrings: PreviewStrings ): Promise { try { const ReactDOMServer = (await import('react-dom/server')).default; return ReactDOMServer.renderToStaticMarkup( React.createElement(FilePreview, { fileName, strings: previewStrings, }) ); } catch (err) { console.error('Error rendering React preview:', err); return `Error rendering preview for ${fileName}
`; } } /** * Translates the file content * @param fileContent The file content * @param translations Translations * @param languageId The language ID * @returns Translated file content */ function translateFileContent( fileContent: Record, translations: TranslationEntry[], languageId: string ): Record { const translatedContent = { ...fileContent }; for (const key of Object.keys(translatedContent)) { if (typeof translatedContent[key] !== 'string') { continue; } translatedContent[key] = getTranslation(translations, key, languageId, translatedContent[key]); } return translatedContent; } ``` This helper file exports two main functions: * `parseFile` – Extracts translatable strings and generates an HTML preview * `buildFile` – Reconstructs the final translated file using strings from Crowdin These functions rely on utilities for reading file content, formatting translations, and generating HTML. You’ll implement those next. ### [Creating Utility Helpers](#creating-utility-helpers) [Section titled “Creating Utility Helpers”](#creating-utility-helpers) This step implements the supporting functions used for parsing file content, generating previews, and preparing files for download or export. These helpers are referenced by `parseFile` and `buildFile`. First, create the TypeScript types that define the data structures: lib/file-utils/types.ts ```ts /** * Types for working with the file system and translations */ /** * Record for a single translation string */ export interface TranslationRecord { text: string; } /** * Record for a single translation string */ export interface TranslationEntry { /** Unique identifier for the string */ identifier: string; /** Context of string usage */ context: string; /** Additional data for the string */ customData: string; /** Preview ID */ previewId: number; /** Labels for the string */ labels: string[]; /** Whether the string is hidden */ isHidden: boolean; /** Original text */ text: string; /** Translations for different languages */ translations: Record; } /** * Information about the file to process */ export interface FileInfo { /** Base64-encoded file content */ content?: string; /** URL to download file content */ contentUrl?: string; /** File name */ name?: string; } /** * Language information */ export interface LanguageInfo { /** Language ID */ id: string; } /** * Request to analyze a file */ export interface ParseFileRequest { /** File information */ file: FileInfo; /** Target languages */ targetLanguages: LanguageInfo[]; } /** * Request to create a file */ export interface BuildFileRequest { /** File information */ file: FileInfo; /** Target languages */ targetLanguages: LanguageInfo[]; /** Strings to translate */ strings?: TranslationEntry[]; /** URL to download strings */ stringsUrl?: string; } /** * Structure of strings for preview */ export interface PreviewStrings { [key: string]: { text: string; id: number; }; } /** * Type of request to the file processing API */ export interface ProcessRequestBody { /** Job type */ jobType: 'parse-file' | 'build-file' | unknown; /** File information */ file: FileInfo; /** Target languages */ targetLanguages: LanguageInfo[]; /** Strings to translate */ strings?: TranslationEntry[]; /** URL to download strings */ stringsUrl?: string; } ``` Create the content processor utilities: lib/file-utils/content-processor.ts ```ts import { FileInfo, TranslationEntry } from './types'; /** * Retrieve and parse the JSON content from the provided `FileInfo` structure. * * The function supports two mutually exclusive sources: * 1. `content` – Base64 encoded string that will be decoded and parsed. * 2. `contentUrl` – Remote URL that will be fetched via HTTP `GET`. * * @throws When neither source is available or when the content cannot be * fetched/parsed. */ export async function getContent(file: FileInfo): Promise> { if (file.content) { try { return JSON.parse(Buffer.from(file.content, 'base64').toString()); } catch (error) { throw new Error( `Failed to parse file content: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } if (file.contentUrl) { try { const response = await fetch(file.contentUrl); if (!response.ok) { throw new Error(`HTTP error: ${response.status} ${response.statusText}`); } return await response.json(); } catch (error) { throw new Error( `Failed to load content from ${file.contentUrl}: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } throw new Error('File object must contain either content or contentUrl'); } /** * Resolve the array of `TranslationEntry` objects that should be used for the * current build/export operation. * * The caller can either inline the strings directly (`req.strings`) or provide * a link to a JSON file (`req.stringsUrl`). The helper normalises both cases * so that the rest of the pipeline receives an in-memory array. * * @throws When neither `strings` nor `stringsUrl` is provided or when the * remote resource fails to load. */ export async function getStringsForExport(req: { strings?: TranslationEntry[]; stringsUrl?: string; }): Promise { if (!req.strings && !req.stringsUrl) { throw new Error('Received invalid data: strings not found'); } if (req.strings) { return req.strings; } if (req.stringsUrl) { try { const response = await fetch(req.stringsUrl); if (!response.ok) { throw new Error(`HTTP error: ${response.status} ${response.statusText}`); } return await response.json(); } catch (error) { throw new Error( `Failed to load strings from ${req.stringsUrl}: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } return []; } /** * Safely obtain a translation string for the requested language or return the * fallback value when a translation is missing. */ export function getTranslation( translations: TranslationEntry[], stringId: string, languageId: string, fallbackTranslation: string ): string { const translation = translations.find( t => t.identifier === stringId && t.translations && t.translations[languageId] ); return translation?.translations[languageId]?.text || fallbackTranslation; } ``` Create the blob storage utilities for handling large files: lib/file-utils/blob-storage.ts ```ts import { put, BlobAccessError } from '@vercel/blob'; import { v4 as uuidv4 } from 'uuid'; const MAX_SIZE_BYTES = 1024 * 1024; // 1MB for data response size /** * Ensure that an RW token for Vercel Blob Storage is present before any upload * is attempted. Throws an `Error` when the token is missing so that the caller * can handle configuration issues gracefully. */ function validateBlobAccess(): void { if (!process.env.BLOB_READ_WRITE_TOKEN) { console.warn( 'BLOB_READ_WRITE_TOKEN is not set. Ensure Vercel Blob Storage is connected to the project.' ); throw new Error('Vercel Blob access token is not configured.'); } } /** * Checks if the content exceeds the maximum size for direct response * @param content The content to check * @returns True if exceeds max size */ export function exceedsMaxSize(content: string): boolean { return Buffer.byteLength(content, 'utf8') > MAX_SIZE_BYTES; } /** * Uploads content to blob storage and returns the URL * @param content The content to upload * @param path The path where to store the content * @param contentType The content type * @returns URL to access the uploaded content */ export async function uploadToBlob( content: string, path: string, contentType: string ): Promise { validateBlobAccess(); try { // Split the path to get directory and filename const lastSlashIndex = path.lastIndexOf('/'); const basePathname = lastSlashIndex >= 0 ? path.substring(0, lastSlashIndex + 1) : ''; const filename = lastSlashIndex >= 0 ? path.substring(lastSlashIndex + 1) : path; if (!filename) { throw new Error('Invalid path: filename cannot be empty'); } const finalBasePath = basePathname || 'uploads/'; const finalFilename = filename; // Ensure contentType is a valid string const validContentType = contentType || 'application/octet-stream'; const blob = await put(finalBasePath + finalFilename, content, { access: 'public', contentType: validContentType, addRandomSuffix: true, }); return blob.url; } catch (error) { console.error('Error uploading to blob:', error); if (error instanceof BlobAccessError) { throw new Error(`Blob access error: ${error.message}`); } throw new Error( `Failed to upload to blob storage: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } /** * Generates a unique filename without extension * @param fileName Original filename * @returns Base filename without extension */ export function generateUniqueFileName(fileName?: string): string { const safeFileName = fileName || `file_${uuidv4()}`; if (safeFileName.includes('.')) { const parts = safeFileName.split('.'); return parts[0] || safeFileName; } return safeFileName; } ``` And create the corresponding React component to render a simple HTML preview: lib/FilePreview\.tsx ```tsx 'use server'; import React from 'react'; import Head from 'next/head'; /** * Describes a single string item that will be displayed in the preview. * Each preview item keeps the original text and the unique identifier that * helps React render a stable list. */ interface PreviewStringItem { text: string; id: number; } /** * A map of string keys (i.e. translation identifiers) to their corresponding * preview information. The key is the original string identifier, while the * value provides a human-readable `text` representation and an `id` used as * a React `key` when rendering lists. */ interface PreviewStrings { [key: string]: PreviewStringItem; } /** * Props accepted by the `FilePreview` React component. */ interface FilePreviewProps { fileName: string; strings: PreviewStrings; } /** * Presentational component that renders a basic HTML preview of the parsed * file. It shows the file name and a list of strings that were extracted * from the file for translation. * * The component is intentionally free of any business logic – it only knows * how to display the data that was already prepared by the server-side * parser. */ const FilePreview: React.FC = ({ fileName, strings }) => { return ( <> Preview: {fileName} File Preview: {fileName}
{Object.keys(strings).length > 0 ? ( {Object.entries(strings).map(([key, value]) => ( - {key}: {value.text}
))}
) : ( No strings to display.
)} > ); }; export default FilePreview; ``` These helpers let your app: * Read the uploaded file’s content * Find the right translations for export * Generate an inline preview using React * Return a static HTML preview to display inside Crowdin In the next step, you’ll optionally add support for large file handling using blob storage. ### [Handling Large Files with Blob Storage](#handling-large-files-with-blob-storage) [Section titled “Handling Large Files with Blob Storage”](#handling-large-files-with-blob-storage) If the processed file data (either strings or preview HTML) exceeds Crowdin’s inline payload size limit (around 5 MB), your app should upload the content to a temporary location and return a download link instead. First, add the Vercel Blob Storage token to your environment variables: .env.local ```ini # Vercel Blob Storage token (for handling large files) BLOB_READ_WRITE_TOKEN= ``` If your app is already deployed to Vercel, update the environment variables in your Vercel dashboard and redeploy. Then, update your `parseFile` and `buildFile` logic to use this helper when needed. For example: ```ts if (Buffer.byteLength(previewHtml, 'utf8') > 5 * 1024 * 1024) { const previewUrl = await uploadToBlob(previewHtml, 'preview.html', 'text/html'); return { data: { strings: sourceStrings, previewUrl, }, }; } ``` This ensures compatibility with larger files while maintaining Crowdin’s required response structure. Crowdin will download the file from the provided URL and process it as if it were inline. ### [Testing the Custom File Format Implementation](#testing-the-custom-file-format-implementation) [Section titled “Testing the Custom File Format Implementation”](#testing-the-custom-file-format-implementation) To verify that your custom file format module is working correctly, upload a test file to any Crowdin project where your app is installed. Use the following example content: ```json { "hello_world": "Hello World!", "test": "This is a sample string for translation" } ``` Save this content to a `.json` file on your local machine (e.g. `sample.json`). Then, open your test project in Crowdin and upload the file via **Sources > Files**. Crowdin will detect that the file matches your custom-file-format signature and send it to your app’s `/api/file/process` route. If everything is set up correctly: * The **file will be parsed**, and two source strings will appear in the Editor. * The **left-side preview panel** will display a rendered HTML view using your app’s preview template. If the file content is large, Crowdin will download the preview from your app’s `previewUrl` instead of using inline data. ## [Updating the App Base URL](#updating-the-app-base-url) [Section titled “Updating the App Base URL”](#updating-the-app-base-url) If your app’s domain changes after it has been installed in Crowdin (for example, after moving from a staging to production environment), you’ll need to update the `baseUrl` and reinstall the app. Crowdin caches the `baseUrl` from the manifest at the time of installation. Updating the environment variable alone is not enough—Crowdin won’t re-read it until you reinstall the app. To update the deployment domain: 1. Set the new value in your hosting environment (e.g. `NEXT_PUBLIC_BASE_URL=https://your-new-domain.vercel.app`). 2. Redeploy your app to apply the change. 3. Open your manifest URL in the browser and confirm that `baseUrl` reflects the new domain. 4. In Crowdin, go to **Account Settings > Crowdin Applications**. 5. Remove the old version of the app. 6. Reinstall the app using the updated manifest URL. After reinstalling, Crowdin will use the updated domain for iframe modules and event delivery. ## [See Also](#see-also) [Section titled “See Also”](#see-also) [Crowdin JS SDK ](https://github.com/crowdin/apps-helpers)Helpers for Crowdin iframe apps. [Environment Variables in Vercel ](https://vercel.com/docs/configuration/project/environment-variables)Define and manage variables in Vercel projects. [Next.js App Router ](https://nextjs.org/docs/app/building-your-application/routing)Docs for the App Router in Next.js 13+.
# Releasing Crowdin Apps
> Learn how to release your Crowdin App to make it accessible to the users
Once the development phase of your Crowdin App is complete, you should publish it to make it accessible to the users and customers. The release process consists of the following stages: * Setting up and deploying the production environment * Publishing your Crowdin App to the [Crowdin Store](https://store.crowdin.com/) ### [Setting up the Production Environment](#setting-up-the-production-environment) [Section titled “Setting up the Production Environment”](#setting-up-the-production-environment) When preparing your Crowdin App to release, ensure that it has both the production and development environments. There are many benefits of having separate environments, but the main ones are the following: * **Stability** – Having multiple environments reduces or eliminates downtime and thus saves the app from losing customers. It will also prevent incidents of loss or deletion of production data. * **Security** – The separation of environments will restrict access to the customers’ data for people who shouldn’t have such access. Crowdin App must use individual OAuth clients, databases, and other credentials between environments. ### [Deploying the Crowdin App](#deploying-the-crowdin-app) [Section titled “Deploying the Crowdin App”](#deploying-the-crowdin-app) When the Crowdin App is ready to be used, you need to host it online to make it accessible to the users. You can choose any preferred platform to your liking. The only requirement is that the platform must be reliable. Below you can see the list of platforms that can be used for deploying: * **Vercel** - provides the developer experience and infrastructure to build, scale, and secure a faster, more personalized web. Read more about [Getting Started](https://vercel.com/docs) and [Deployment](https://vercel.com/docs/platform/deployments). * **Heroku** - a container-based cloud Platform as a Service (PaaS). Read more about [Getting Started Guide on Heroku](https://devcenter.heroku.com/start) and [Deployment](https://devcenter.heroku.com/categories/deployment). * **DigitalOcean** - a cloud computing vendor that offers an infrastructure as a service (IaaS) platform for software developers. Read more about [Getting Started](https://docs.digitalocean.com/products/app-platform/how-to/#getting-started) and [Dev Guide](https://docs.digitalocean.com/products/app-platform/languages-frameworks/). * **Amazon Web Services** - an online platform that provides scalable and cost-effective cloud computing solutions. * **Google Cloud** - a platform offering scalable cloud computing services, data analytics, and tools for building and managing applications. #### [Hosting Your Crowdin App on Crowdin Servers](#hosting-your-crowdin-app-on-crowdin-servers) [Section titled “Hosting Your Crowdin App on Crowdin Servers”](#hosting-your-crowdin-app-on-crowdin-servers) All developers who create their own Crowdin Apps have the opportunity to host their apps on the Crowdin servers. To take advantage of this option, app developers are required to submit their app’s source code to Crowdin for review and deployment. Our team will review the source code, and once approved, the app will be hosted by Crowdin. Each app hosting arrangement comes with custom and personalized agreements tailored specifically to the app. Additionally, apps hosted by Crowdin receive a special Verified by Crowdin badge, instilling confidence in potential app users and increasing the likelihood of installation and usage. [Contact Support Team](https://crowdin.com/contacts), and we will be happy to provide you with more information on the matter. ### [Distributing the Crowdin App](#distributing-the-crowdin-app) [Section titled “Distributing the Crowdin App”](#distributing-the-crowdin-app) Once you’re ready to share your Crowdin App with users, [Contact Support Team](https://crowdin.com/contacts) to provide you with a developer account on [Crowdin Store](https://store.crowdin.com). The developer account lets you publish Crowdin Apps in the Crowdin Store and view different statistics such as the number of installations, application rating, etc.
# Security for Crowdin Apps
> Ensure the high level of security for Crowdin apps
To ensure the high level of security for cases when the Crowdin app works with the data from Crowdin (i.e. uses the authorization via `crowdin_app`), we’ve developed a security mechanism. The main principle of this security mechanism is based on the exchange of the JWT token between Crowdin and the Crowdin app. JWT token is signed with an OAuth Client Secret known only to the two final parties. This way, the Crowdin app can get a confirmation that the page is opened precisely in Crowdin. ## [Implementation](#implementation) [Section titled “Implementation”](#implementation) To implement the authorization and authentication in your Crowdin app, follow these steps: * Add the authorization via `crowdin_app` to your app descriptor and add the OAuth Client ID that will be used for authorization. * Add the callback to your Crowdin app that will handle the [Installed Event](/developer/crowdin-apps-installation/#installed-event-communication-flow). * Specify the necessary set of scopes in your app descriptor needed for your Crowdin app. The specified set of scopes shouldn’t exceed the scopes specified in the OAuth. Using the above methods, on each request to the Crowdin app, Crowdin will pass a set of parameters along with a security token, which can be validated by a secret from the OAuth. Below you can see an example of the URL used by Crowdin to open a module page. ```shell https://example.com/app-module?jwtToken=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJCcjRhMmhwUW……MX0.yt-lbv3Z8JyIGX4jG405mjZvX8lwc1q0EfWdTtm9GCc&origin=https://{domain}.crowdin.com&clientId=your-client-id ``` Query parameters: | | | | ---------- | ------------------------------------------------------------------------------------- | | `jwtToken` | **Type:** `string`**Description:** JWT token used for authorization. | | `origin` | **Type:** `string (url)`**Description:** Host used for opening a module page. | | `clientId` | **Type:** `string`**Description:** The ID of the OAuth Client used for authorization. | The best practice would be adding middleware to the Crowdin app to verify whether each request has a token with a valid signature and expiry. You can use one of the [existing libraries](https://jwt.io/) to validate the authenticity of the token. ## [JWT Token Structure](#jwt-token-structure) [Section titled “JWT Token Structure”](#jwt-token-structure) JWT token consists of the following parts: * Header - contains information about the type of the token and encoding algorithms. * Payload - contains additional information about the issue and expiration dates of the token, the information about the token issuer and requestor, and other contextual information. * Signature - the part with a signature based on the header and payload. JWT token payload example: ```json { "aud": "Br4a2hpQiNW96anuuO4a", "sub": "1", "domain": null, "context": {}, "iat": 1600000000, "exp": 1600000900 } ``` Properties: | | | | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `aud` | **Type:** `string`**Description:** ID of the OAuth Client that issued the token. | | `sub` | **Type:** `string`**Description:** Identifier of the user that is making a request to the Crowdin app. | | `domain` | **Type:** `string\|null`**Required:** yes**Description:** The name of the organization from which the app is accessed. For Crowdin the domain value is always `null` | | `context` | **Type:** `object`**Description:** The information about the environment where the Crowdin app module is opened (e.g. project, locale, user’s timezone, etc.). | | `iat` | **Type:** `integer`**Description:** Identifies the issue time of the token. | | `exp` | **Type:** `integer`**Description:** Identifies the expiration time of the token. |
# User Interface
> Learn how to make your Crowdin App look good and blend in with Crowdin UI
UI is an essential part of any application. Appealing looks and a well-thought-out intuitive interface will make your Crowdin App more user-friendly and thus attract the audience to use it. We recommend using frameworks to make your app look good and blend in with Crowdin UI. Below you can see a short list of the frameworks you can use for your Crowdin App. Though, you can use any framework for your UI development. This list carries only an informative purpose. [Bootstrap ](https://getbootstrap.com/)Powerful, extensible, and feature-packed frontend toolkit. [React Bootstrap ](https://react-bootstrap.github.io/)Bootstrap components built with React. [MUI ](https://mui.com/)React components that implement Google's Material Design. [Semantic UI ](https://semantic-ui.com/)A framework that helps create layouts using human-friendly HTML. [shadcn/ui ](https://ui.shadcn.com/)Beautifully designed components that you can copy and paste into your apps. [Crowdin UI kit ](https://crowdin-web-components.s3.amazonaws.com/index.html)Crowdin UI kit for building Crowdin Apps.
# Dev Tools
> Automate the localization process and integrate it into your development workflow
Keep developing new features and improvements while translators receive new texts in real time. Release multilingual versions for customers around the globe simultaneously. Crowdin provides a set of tools that help you to automate the localization process and integrate it into your development workflow. You can use these tools to manage translations, automate file synchronization, and deliver translations to your users. ## [CLI](#cli) [Section titled “CLI”](#cli) Crowdin CLI (Console Client) - a powerful command-line tool that simplifies the management of your localization projects on Crowdin. With Crowdin CLI, you can easily upload source files, download translations, and keep your localized content up-to-date with just a few simple commands.  This is a cross-platform, and it runs in a terminal on Linux based and macOS operating systems or in Command Prompt on Windows. [View Documentation ](https://crowdin.github.io/crowdin-cli/)[Give Feedback ](https://github.com/crowdin/crowdin-cli/discussions) ## [GitHub Action](#github-action) [Section titled “GitHub Action”](#github-action) The Crowdin GitHub Action makes it easy to manage and synchronize localization assets between your GitHub repository and your Crowdin project. By adding this action to your GitHub workflow, you can automate the localization process and keep your project up to date with the latest translations. * Upload sources to Crowdin. * Upload translations to Crowdin. * Downloads translations from Crowdin. * Download sources from Crowdin. * Creates a PR with the translations. * Run any Crowdin CLI command. [View Documentation ](https://github.com/marketplace/actions/crowdin-action)[Give Feedback ](https://github.com/crowdin/github-action/discussions) ## [Content Delivery SDKs](#content-delivery-sdks) [Section titled “Content Delivery SDKs”](#content-delivery-sdks) Crowdin offers SDKs that enable over-the-air delivery of translations to your users. By integrating these SDKs into your mobile or web applications, you can seamlessly update localized content without needing to release a new app version. Official SDKs are available for Android, iOS, Flutter, and websites (JS). [Official ](https://github.com/crowdin/mobile-sdk-android) [](https://github.com/crowdin/mobile-sdk-android) ##### [Android SDK](https://github.com/crowdin/mobile-sdk-android) [View and Install](https://github.com/crowdin/mobile-sdk-android) [Official ](https://github.com/crowdin/mobile-sdk-ios) [](https://github.com/crowdin/mobile-sdk-ios) ##### [iOS SDK](https://github.com/crowdin/mobile-sdk-ios) [View and Install](https://github.com/crowdin/mobile-sdk-ios) [Official ](https://github.com/crowdin/flutter-sdk) [](https://github.com/crowdin/flutter-sdk) ##### [Flutter SDK](https://github.com/crowdin/flutter-sdk) [View and Install](https://github.com/crowdin/flutter-sdk) [Official ](https://github.com/crowdin/ota-client-js) [](https://github.com/crowdin/ota-client-js) ##### [OTA JS Client](https://github.com/crowdin/ota-client-js) [View and Install](https://github.com/crowdin/ota-client-js) ## [IDE Plugins](#ide-plugins) [Section titled “IDE Plugins”](#ide-plugins) Integrate your Visual Studio Code or Android Studio projects with Crowdin to simplify the localization process. Crowdin’s IDE plugins allow you to instantly upload new source strings to your Crowdin project, autocomplete string keys, monitor translation progress, and download translations directly from Crowdin. [Visual Studio Code Plugin ](https://store.crowdin.com/visual-studio-code)Optimize the localization process for your source files in Visual Studio Code projects with Crowdin. [Android Studio Plugin ](https://store.crowdin.com/android-studio)Automatically upload new source strings to Crowdin and instantly download translated content.
# Editor Themes
> Create and distribute custom themes for the Crowdin Editor
Crowdin Editor uses JSON-based files to manage its visual themes. This article outlines the key concepts for designers to create and distribute custom themes. [Explore Existing Themes ](https://store.crowdin.com/collections/themes)Browse the collection of available themes in the Crowdin Store. ## [Theme File Structure](#theme-file-structure) [Section titled “Theme File Structure”](#theme-file-structure) A complete Crowdin theme file contains the following top-level properties: * `themeMode` – accepts `light` or `dark` for base editor styling. * `primaryAccent` – main accent color used throughout the interface. * `base` – a collection of properties controlling various interface elements (backgrounds, font colors, string status colors, highlights). * `accents` – colors used for notifications (info, danger, warning, success messages). * `button` – provides styling options for different button types. ## [Important Notes](#important-notes) [Section titled “Important Notes”](#important-notes) * **Color Values** – all color properties require valid CSS color values (HEX, RGB, RGBA). * **Complex Backgrounds** – properties `base.commonUi.headerBackground`, `base.commonUi.subHeaderBackground`, `base.commonUi.mainContentBackground` and `base.commonUi.mainMenuBackground` accept gradients, images, and other valid CSS background values. * **Minimal Themes** – it’s not mandatory to define every property. Crowdin automatically calculates missing values based on the provided settings. ### [Development and Testing](#development-and-testing) [Section titled “Development and Testing”](#development-and-testing) During the theme development process, we recommend using the [Theme Builder](https://store.crowdin.com/theme-builder) app, with the help of which designers can easily test and preview their themes in real time. ### [Minimal JSON Theme](#minimal-json-theme) [Section titled “Minimal JSON Theme”](#minimal-json-theme) ```json { "themeMode": "dark", "primaryAccent": "#f9d9c8", "base": { "baseBackground": "#2d2b52", "stringStatus": { "translated": "#5b89c6", "approved": "#6dc271" }, "highlights": { "placeholderColor": "#afff8a", "placeholderBg": "#74827A", "tagColor": "#bfc3a0", "tagBg": "#626550", "nonePrintableCharacterColor": "#3eb17f", "findAndReplaceHighlightBg": "#8A6800", "specialLightColor": "#E0E6ED", "specialLightBg": "#383E47" } }, "accents": { "info": { "accentColor": "#35a1ff" }, "danger": { "accentColor": "#fa644a" }, "warning": { "accentColor": "#cc9a06" }, "success": { "accentColor": "#74bb02" } } } ``` ### [Full JSON Theme](#full-json-theme) [Section titled “Full JSON Theme”](#full-json-theme) ```json { "themeMode": "light", "primaryAccent": "#4A90E2", "base": { "baseBackground": "#FFFFFF", "cardsBackground": "#F7F7F7", "typeface": { "baseColor": "black", "bodyColor": "black", "mutedColor": "black", "disabledColor": "black", "iconsColor": "black" }, "commonUi": { "headerBackground": "#4A90E2", "subHeaderBackground": "#3E7CB1", "mainContentBackground": "#FFFFFF", "checkedStringBackground": "#E0F7FA", "mainMenuBackground": "#2E3A4A" }, "scrollBars": { "thumbColor": "#ffffff", "trackColor": "#f5f7f8" }, "borders": { "borderColor": "#263238", "darkBorderColor": "#000000" }, "stringStatus": { "translated": "#4CAF50", "approved": "#0A8F08", "notTranslated": "#FFC107", "hidden": "#9E9E9E" }, "highlights": { "placeholderColor": "#9E9E9E", "placeholderBg": "#ECEFF1", "tagColor": "#4A90E2", "tagColorHover": "#357ABD", "tagBg": "#ECEFF1", "tagBgHover": "#CFD8DC", "nonePrintableCharacterColor": "#FF5252", "findAndReplaceHighlightBg": "#FFEB3B", "specialLightColor": "#770000", "specialLightBg": "#F0F0FF" } }, "accents": { "info": { "accentColor": "#2196F3", "backgroundColor": "#E3F2FD" }, "danger": { "accentColor": "#F44336", "backgroundColor": "#FFEBEE" }, "warning": { "accentColor": "#FF9800", "backgroundColor": "#FFF3E0" }, "success": { "accentColor": "#4CAF50", "backgroundColor": "#E8F5E9" } }, "button": { "default": { "btnColor": "#333333", "btnHoverColor": "#444444", "btnBorder": "#BDBDBD", "btnHoverBorder": "#9E9E9E", "btnActiveBorder": "#757575", "btnDisabledBorder": "#E0E0E0", "btnBg": "#E0E0E0", "btnHoverBg": "#EEEEEE", "btnActiveBg": "#BDBDBD", "btnDisabledBg": "#F5F5F5", "btnModalBorder": "#9E9E9E" }, "primary": { "btnColor": "#FFFFFF", "btnBorder": "#4A90E2", "btnBg": "#2196F3", "btnHoverBg": "#1976D2", "btnActiveBg": "#0D47A1" }, "danger": { "btnBg": "#F44336", "btnHoverBg": "#D32F2F", "btnBorder": "#FFCDD2", "btnHoverBorder": "#EF9A9A" }, "icon": { "btnBg": "#FFFFFF", "btnBorder": "#BDBDBD", "btnHoverBorder": "#9E9E9E", "btnActiveBorder": "#757575" } } } ``` ## [Theme Publishing](#theme-publishing) [Section titled “Theme Publishing”](#theme-publishing) Custom themes should be submitted to the Crowdin Store to become available for installation by Crowdin users. To publish a theme, follow these steps: 1. [Contact Support Team](https://crowdin.com/contacts): We will handle the review and publishing process 24x7. 2. Provide the following information: * Author name * Theme name * Description * Screenshot of the theme in use
# GraphQL API
> Retrieve exactly the data you need using more specific and flexible queries
GraphQL API is a tool that allows you to retrieve exactly the data you need using more specific and flexible queries. One of the main benefits of GraphQL API is that you can get many different resources using a single request. For cases when you want to run queries against the Crowdin GraphQL API, we recommend using the GraphQL Playground app, with the help of which you can build, test, and debug queries from Crowdin and Crowdin Enterprise web UI even before writing any code in your application. [GraphQL Playground ](https://store.crowdin.com/graphql-playground)Integrated GraphQL IDE for better development workflow. ## [Authorization with GraphQL API](#authorization-with-graphql-api) [Section titled “Authorization with GraphQL API”](#authorization-with-graphql-api) To work with GraphQL API in Crowdin or Crowdin Enterprise, use one of the following access tokens: * [Crowdin Personal Access Token](/account-settings/#creating-a-personal-access-token) * [Crowdin Enterprise Personal Access Token](/enterprise/account-settings/#creating-a-personal-access-token) * [OAuth Access Token](/developer/authorizing-oauth-apps/#make-requests-to-the-api-with-the-access-token-returned) Ensure to use the following header in your requests: ```bash Authorization: Bearer ``` The response in case authorization fails: 401 Unauthorized ```json { "error": { "message": "Unauthorized", "code": 401 } } ``` ## [Root Endpoint](#root-endpoint) [Section titled “Root Endpoint”](#root-endpoint) In contrast to the REST API, GraphQL API has only one endpoint that remains constant, not depending on the performed operations. Crowdin GraphQL endpoint: ```bash https://api.crowdin.com/api/graphql ``` Crowdin Enterprise GraphQL endpoint: ```bash https://{domain}.api.crowdin.com/api/graphql ``` ## [Resource Limitations](#resource-limitations) [Section titled “Resource Limitations”](#resource-limitations) The Crowdin GraphQL API has limitations to prevent excessive or abusive calls to Crowdin servers. ### [Node Limit](#node-limit) [Section titled “Node Limit”](#node-limit) All GraphQL API calls must comply with the following requirements to pass schema validation: * Users must supply a `first` or `last` argument on any connection. * Values of `first` and `last` must be within 1-10,000. * Individual calls can’t request more than 10,000 total nodes. #### [Nodes Calculation](#nodes-calculation) [Section titled “Nodes Calculation”](#nodes-calculation) In the following examples, you can check out how the nodes in a call are calculated. ##### [Simple query](#simple-query) [Section titled “Simple query”](#simple-query) ```graphql query { viewer { projects(first: 50) { edges { node { name files(first: 10) { totalCount edges { node { name type } } } } } } } } ``` Calculation: ```plaintext 50 = 50 projects + 50 x 10 = 500 files = 550 total nodes ``` ##### [Complex query](#complex-query) [Section titled “Complex query”](#complex-query) ```graphql query { viewer { projects(first: 50) { edges { node { files(first: 20) { edges { node { strings(first: 10) { edges { node { ... on PlainSourceString { text } ... on ICUSourceString { text } ... on PluralSourceString { plurals { one other } } ... on AssetSourceString { text } } } } } } } translations(first: 20, languageId: "uk") { edges { node { ... on PlainStringTranslation { text } ... on ICUStringTranslation { text } ... on PluralStringTranslation { pluralForm text } ... on AssetStringTranslation { text } } } } } } } } } ``` Calculation: ```plaintext 50 = 50 projects + 50 x 20 = 1,000 files + 50 x 20 x 10 = 10,000 strings + 50 x 20 = 1,000 translations = 12,050 total nodes ``` ### [Rate Limit](#rate-limit) [Section titled “Rate Limit”](#rate-limit) The GraphQL API limit is quite different compared to the [REST API Rate Limits](/developer/api/v2/#section/Introduction/Rate-Limits). As mentioned above, you can get the same amount of data using only one GraphQL call and replacing the need to execute multiple REST calls. While a single complex GraphQL call could be equivalent to thousands of REST requests and wouldn’t exceed the REST API rate limit, its computation might be just as expensive for Crowdin servers. The GraphQL API uses a normalized point scale to precisely depict the server cost of a query by calculating a call’s rate limit score. This score includes the first and last arguments on a parent connection and its children. * The formula uses the `first` and `last` arguments on a parent connection and its children to pre-determine the possible load on Crowdin systems, such as MySQL and ElasticSearch. * Each new connection has its own point value. Points are added to the call’s other points to form a final rate limit score. The GraphQL API rate limit is set to 5,000 points per hour. Since the GraphQL API and REST API use different rate limits, 5,000 points per hour aren’t the same as 5,000 calls per hour. Caution The current calculation method and rate limit aren’t constant and might be changed in the future. #### [Checking Rate Limit Status of a Call](#checking-rate-limit-status-of-a-call) [Section titled “Checking Rate Limit Status of a Call”](#checking-rate-limit-status-of-a-call) To check the rate limit status when using GraphQL API, query the fields on the `rateLimit` object: ```graphql query { viewer { username } rateLimit { limit cost remaining resetAt } } ``` * `limit` – returns the maximum number of points the user is allowed to consume in a 60-minute window. * `cost` – returns the point cost for the current call that counts against the rate limit. * `remaining` – returns the number of points remaining in the current rate limit window. * `resetAt` – returns the time at which the current rate limit window resets in UTC epoch seconds. #### [Estimating Rate Limit Score before Call Execution](#estimating-rate-limit-score-before-call-execution) [Section titled “Estimating Rate Limit Score before Call Execution”](#estimating-rate-limit-score-before-call-execution) While querying the `rateLimit` object can give you a call’s score, it counts against the limit. To work around this situation, you can estimate the score of a call in advance. Using the following calculation, you can get approximately the same cost as returned by `rateLimit { cost }`. 1. First, the number of requests required to fulfill each unique connection in the call should be added up. Suppose each request will reach the `first` or `last` argument limits. 2. Next, you need to divide the number by 100 and round the result to obtain the final combined cost. This step normalizes large numbers. Here’s an example query and score calculation: ```graphql query { viewer { username projects(first: 100) { edges { node { id files(first: 50) { edges { node { id strings(first: 60) { edges { node { ... on PlainSourceString { id text } ... on ICUSourceString { id text } ... on PluralSourceString { id plurals { one other } } ... on AssetSourceString { id text } } } } } } } } } } } } ``` * While returning 100 projects, the API has to connect to the user’s account once to get the list of projects. So, requests for projects = 1 * While returning 50 files, the API has to connect to each of the 100 projects to get the list of files. So, requests for files = 100 * While returning 60 strings, the API has to connect to each of the 5,000 potential total files to get the list of strings. So, requests for strings = 5,000 * Total = 5,101 Now, divide the total of 5,101 by 100 and round it. As a result, you get the final score of the query, which is 51. ## [Pagination](#pagination) [Section titled “Pagination”](#pagination) Pagination is a fundamental concept in GraphQL that allows you to retrieve a subset of data from a larger collection, making it easier to manage and display information. In this section, we’ll explore how to use pagination in Crowdin GraphQL API, focusing on the `projects` field as an example. ### [Understanding Pagination in Crowdin GraphQL](#understanding-pagination-in-crowdin-graphql) [Section titled “Understanding Pagination in Crowdin GraphQL”](#understanding-pagination-in-crowdin-graphql) Before diving into the specifics of using pagination, let’s clarify some key terms: * **Connection** – In Crowdin GraphQL, a connection is a structure that holds a list of items. It typically includes edges, pageInfo, and totalCount. Edges contain the actual data items, pageInfo provides information about pagination, and totalCount indicates the total number of items in the connection. * **Edges** – Edges are individual items within a connection. Each edge contains the node (the data item) and a cursor, which is a string used to navigate the collection. * **PageInfo** – PageInfo provides information that helps you determine if there are more items to retrieve. It includes fields like `hasNextPage`, `hasPreviousPage`, `startCursor`, and `endCursor`. ### [Using Pagination in Crowdin GraphQL](#using-pagination-in-crowdin-graphql) [Section titled “Using Pagination in Crowdin GraphQL”](#using-pagination-in-crowdin-graphql) Now, let’s focus on using pagination with the `projects` field in the Crowdin GraphQL API. #### [Querying Projects with Pagination](#querying-projects-with-pagination) [Section titled “Querying Projects with Pagination”](#querying-projects-with-pagination) The `projects` field within the `User` type is used to query the projects associated with a user. It accepts several input parameters that allow you to control the pagination of the results. These parameters are: * `after` – A cursor that indicates where the query should start from in the list of projects. * `first` – The number of projects to retrieve after the specified cursor. * `before` – A cursor that indicates where the query should end. * `last` – The number of projects to retrieve before the specified cursor. #### [Example Query](#example-query) [Section titled “Example Query”](#example-query) The following example query requests the first ten projects associated with the authenticated user starting from the provided cursor (`cursor_string`). The response will include the `edges` array containing the project data, as well as `pageInfo` and `totalCount` fields for pagination control. ```graphql query { viewer { projects( after: "cursor_string", # Replace with a valid `after` cursor first: 10 ) { edges { node { id name description # Add more fields as needed } cursor } pageInfo { hasNextPage hasPreviousPage startCursor endCursor } totalCount } } } ``` Here is a response example for the above query: ```json { "data": { "viewer": { "projects": { "edges": [ { "node": { "id": 1, "name": "Umbrella", "description": "Official Umbrella Translation Project" }, "cursor": "MA==" }, { "node": { "id": 2, "name": "Umbrella iOS", "description": "Official Umbrella iOS App Translation Project" }, "cursor": "MQ==" } ], "pageInfo": { "hasNextPage": true, "hasPreviousPage": false, "startCursor": "MA==", "endCursor": "MQ==" }, "totalCount": 5 } } } } ``` #### [Pagination Control](#pagination-control) [Section titled “Pagination Control”](#pagination-control) * `hasNextPage` – This field in `pageInfo` indicates whether there are more projects available for the next page. * `hasPreviousPage` – This field in `pageInfo` indicates whether there are more projects available for the previous page. * `startCursor` – The cursor pointing to the first project in the current result set. * `endCursor` – The cursor pointing to the last project in the current result set. * `totalCount` – The total count of projects associated with the user. #### [Fetching Previous Pages](#fetching-previous-pages) [Section titled “Fetching Previous Pages”](#fetching-previous-pages) There are scenarios where you may need to navigate backward through pages in your dataset. This might be needed for various reasons, such as: reviewing older data, correction or modification, etc. To navigate backward through pages, you can use the `last` and `before` parameters. The `last` parameter specifies the number of items from the end of the list, and the `before` parameter takes the cursor of the first item you want to retrieve. Here’s an example: ```graphql query { viewer { projects( last: 10, before: "cursor_of_first_item" # Replace with a valid `before` cursor ) { edges { node { id name description # Add more fields as needed } } } } } ``` ## [Filtering and Sorting](#filtering-and-sorting) [Section titled “Filtering and Sorting”](#filtering-and-sorting) Crowdin GraphQL provides capabilities for filtering and sorting (ordering) data, allowing you to narrow down the selection of data you want to retrieve and arrange it in a particular order. As with pagination, in this section, we’ll explore how to use filtering and sorting in Crowdin GraphQL API, focusing on the `projects` field as an example. ### [Filtering Data](#filtering-data) [Section titled “Filtering Data”](#filtering-data) Filtering is the process of specifying criteria to select a subset of data from a larger dataset. In Crowdin GraphQL, filtering lets you narrow down your query results based on specific conditions. This is particularly useful when you want to retrieve data that meets specific requirements or characteristics. #### [`ProjectFilterInput`](#projectfilterinput) [Section titled “ProjectFilterInput”](#projectfilterinput) Crowdin GraphQL provides the `ProjectFilterInput` type, which allows you to filter projects based on various attributes. Here are some key attributes you can filter by: * `and` – A logical conjunction that combines multiple filter criteria. * `or` – A logical disjunction that combines multiple filter criteria. * `id` – Filter by project ID using various conditions, such as equality, greater than, or less than. * `userId` – Filter projects by the user ID associated with them. * `name` – Filter project by their name, with options to check for equality, containment, or starting with specific text. * `identifier` – Filter by project identifier, similar to filtering by name. * `description` – Filter project by their description, with options for equality, containment, or starting with specific text. * `publicDownloads` – Filter projects based on whether public downloads are enabled. * `languageAccessPolicy` – Filter projects by their language access policy (e.g., “open” or “moderate”). * `visibility` – Filter projects based on their visibility (e.g., “open” or “private”). * `createdAt` – Filter projects by their creation date using various date-related conditions. * `updatedAt` – Filter projects by their last update date. * `lastActivityAt` – Filter projects by their last activity date. Filtering is a flexible way to target the data you need in your queries precisely. You can use logical operators such as `and` and `or` to combine multiple filtering conditions to refine your query even further. #### [Example of Filtering](#example-of-filtering) [Section titled “Example of Filtering”](#example-of-filtering) To retrieve projects created after a specific date and with a “private” visibility setting, you can create a filter input like this: ```graphql query { viewer { projects( first: 10, filter: { createdAt: { gt: "2023-01-01T00:00:00Z" } and: { visibility: { equals: private } } } ) { edges { node { id name description # Add more fields as needed } } } } } ``` This filter will return projects that meet both conditions: created after January 1, 2023, and set to “private” visibility. ### [Sorting Data](#sorting-data) [Section titled “Sorting Data”](#sorting-data) Sorting involves specifying the order in which query results are presented. It doesn’t reduce the number of results but arranges them in a particular sequence. Crowdin GraphQL provides options for sorting data based on attributes such as project name, creation date, or other relevant factors. #### [`ProjectOrderInput`](#projectorderinput) [Section titled “ProjectOrderInput”](#projectorderinput) The `ProjectOrderInput` type in Crowdin GraphQL allows you to specify the sorting order for your query results. You can order projects based on attributes like: * `id` – Sort projects by their unique identifier. * `userId` – Sort projects by the identifier of the user who created them. * `name` – Sort projects by their name. * `identifier` – Sort projects by their identifier. * `description` – Sort projects by their description. * `publicDownloads` – Sort projects by their public download setting. * `languageAccessPolicy` – Sort projects by their language access policy. * `visibility` – Sort projects by their visibility setting. * `createdAt` – Sort projects by their creation date. * `updatedAt` – Sort projects by their last update date. * `lastActivityAt` – Sort projects by their last activity date. You can specify the sorting order to be in ascending (“asc”) or descending (“desc”) order, giving you complete control over how the data is presented. #### [Example of Sorting](#example-of-sorting) [Section titled “Example of Sorting”](#example-of-sorting) To retrieve projects sorted by their name in descending order, you can create a sort input like this: ```graphql query { viewer { projects( first: 10 order: [{ name: desc }] ) { edges { node { id name description # Add more fields as needed } } } } } ``` This sorting order will present the projects in reverse alphabetical order of their names. ### [Combining Filtering and Sorting](#combining-filtering-and-sorting) [Section titled “Combining Filtering and Sorting”](#combining-filtering-and-sorting) Crowdin GraphQL allows you to combine both filtering and sorting to tailor your queries precisely. You can first filter data to select a subset that meets specific criteria and then sort the results in the desired order. This combination allows you to retrieve and arrange data according to your specific requirements. #### [Example of Filtering and Sorting](#example-of-filtering-and-sorting) [Section titled “Example of Filtering and Sorting”](#example-of-filtering-and-sorting) To retrieve projects created after a specific date, with the “moderate” language access policy, and sort them by their last activity date in ascending order, you can create a query like this: ```graphql query { viewer { projects( first: 10 filter: { createdAt: { gt: "2023-01-01T00:00:00Z" } and: { languageAccessPolicy: { equals: moderate } } }, order: [{ lastActivityAt: asc }] ) { edges { node { id name description # Add more fields as needed } } } } } ``` This query will return projects that meet the filtering conditions and present them in ascending order based on their last activity date.
# In-Context
> Translate your web application directly in the real-time context
Crowdin In-Context works with the help of a one-line Javascript snippet and pseudo-language package. It creates a pseudo-language package based on the localization files uploaded to your project, which later will be integrated into your application as an additional localization language. ## [Quick Demo](#quick-demo) [Section titled “Quick Demo”](#quick-demo) [Play](https://youtube.com/watch?v=ktfw7UsW3qw) In-Context localization is tied up with your Crowdin project, where translatable files are stored. Translations made using In-Context are added to your project in the same way as translations made directly in the Editor. This tool provides a view of your application with editable texts, so the translation process is real-time visible. Even the dynamic part of the application and strings containing placeholders can be translated this way. [See in Action ](https://demo.crowdin.com/)[Get Personalized Demo ](https://crowdin.com/demo-request) Integrated pseudo-language contains special identifiers instead of the original texts. Thus when switching your app to that language, all labels are converted to special identifiers. Javascript searches for those identifiers and replaces them with editable labels. So, the In-context page of your web app will look the same as your application, with the only difference that translatable strings will be editable.  Translations are made directly in the app, with no need to open the Editor. A simplified version of Crowdin Editor will be displayed, with all the functionality (TM, machine translation, approve/vote option, comments, terms) provided. Thus, it’s easier to make and review translations since translators can preview them in a real context. ## [Integration](#integration) [Section titled “Integration”](#integration) There are two common approaches to integrate Crowdin In-Context with your application: * **Staging or translation environment** If you are not planning to invite your end-users to help with translations or not considering using a “translation mode” in your production application, integrating Crowdin In-Context to your staging or dedicated translation app environment would be a good solution. * **Production environment** Crowdin In-Context doesn’t require any code changes in your application so that you can use it even on production. You decide how to turn it on and which segment of users will use it. The most common use cases are the following: * You can create a mirror website that is a complete copy of your application but under a different URL (for example, instead of `crowdin.com` it can be `translate.crowdin.com`), where translators will make translations as if it was your actual application. * You can also add In-context as an additional language. So, when translators open your application, they will choose this additional language from the list, which will open In-context for them. Follow the integration setup guide in your Crowdin project to set up In-Context. The guide can be found in your project, under **Tools > In-Context**. * Crowdin  * Crowdin Enterprise  After the integration is successfully set up and you have refreshed your application, you should see the invitation dialog and Crowdin login box.  ### [Adding String URLs to Context](#adding-string-urls-to-context) [Section titled “Adding String URLs to Context”](#adding-string-urls-to-context) When integrating In-Context for your website, you can add an optional script that can collect and add URLs to the [context section](/online-editor/#requesting-context) of each string used on the website. As a result, translators can click the URL for a particular string and be redirected to the website page where this string is used and get additional context for accurate translation. To add string URLs to context, follow these steps: 1. Copy the following JavaScript snippet and paste it right after the primary In-Context JavaScript snippet (which could be found in your Crowdin project’s **Tools** tab) at the `head` section on every page with localizable text. ```html ``` Once finished, each website’s page you’d like to translate via In-Context should contain two JavaScript snippets: * Primary JavaScript snippet that launches the In-Context feature. * Additional JavaScript snippet that collects and adds URLs to the source string context. 2. After adding JavaScript snippets, you should open each website page where In-Context is integrated. That will allow the additional JavaScript snippet to collect and add string URLs to your Crowdin project. When you add a new page to your website, upload the related source files to your Crowdin project. Afterward, repeat the steps above to collect and add URLs for strings from the new page. If some website page’s URL changes, open it in In-Context to refresh the string URLs collected initially. ## [Optional Parameters](#optional-parameters) [Section titled “Optional Parameters”](#optional-parameters) You can add these parameters to the `_jipt` array in the configuration snippet. ### [Texts Preloading](#texts-preloading) [Section titled “Texts Preloading”](#texts-preloading) ```js _jipt.push(['preload_texts', true]); ``` Speeds up dynamic content displaying within the In-Context tool by preloading all source strings. Automatically disabled for large projects (5000+ strings). Acceptable values: `true`, `false`. ### [Translation Button Always Visible](#translation-button-always-visible) [Section titled “Translation Button Always Visible”](#translation-button-always-visible) ```js _jipt.push(['touch_optimized', true]); ``` This option is enabled on touchscreens by default and makes the translation button next to each translatable string permanently visible instead of showing on hover. Acceptable values: `true`, `false`. ### [Before Commit Callback](#before-commit-callback) [Section titled “Before Commit Callback”](#before-commit-callback) ```js _jipt.push(['before_commit', function(source, translation, context, language_code) { return status_obj; }]); ``` Function to validate the suggestion before committing. | | | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Parameters** | | | source | Source text | | translation | Translation text | | context | Source string context | | language\_code | Target language code ([language codes](/developer/language-codes/)) | | **Return Values** | | | status\_obj | Object. The `status_obj.status` may be `“ok”`, `“error”` or `“corrected”`. In case of error, `status_obj.message` contains error description. When status is `corrected`, `status_obj.correction` contains the corrected translation | ### [Before DOM Insert Callback](#before-dom-insert-callback) [Section titled “Before DOM Insert Callback”](#before-dom-insert-callback) ```js _jipt.push(['before_dom_insert', function(text, node, attribute) { return 'text'; }]); ``` Function to transform the string before it gets inserted into the DOM. | | | | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | | **Parameters** | | | text | String for insertion | | node optional | DOM element in which the text must be inserted. It may be omitted if the text doesn’t have a target element (text inside the browser’s pop-ups) | | attribute optional | The attribute of DOM element, if the text is part of an attribute | | **Return Values** | | | text | String for insertion | | false | If the function returns *false*, DOM will not be updated | ### [Close the In-Context Overlay Callback](#close-the-in-context-overlay-callback) [Section titled “Close the In-Context Overlay Callback”](#close-the-in-context-overlay-callback) ```js _jipt.push(['escape', function() { window.location.href = "http://app_domain.com"; }]); ``` If defined, users can close the In-Context overlay if they don’t want to translate. Implement this feature on your side. Depending on the In-Context integration approach, it must change the app’s language or redirect from the translation environment to the production app.  ### [Start Type](#start-type) [Section titled “Start Type”](#start-type) ```js _jipt.push(['start_type', 'manual']); ``` Defines how In-Context is initialized. Acceptable values: * **`default`** – In-Context starts automatically after the script loads. * **`manual`** – In-Context starts only when you call [`window.jipt.start()`](#start-and-stop-methods). Use `manual` if you need greater control. For example, in single-page apps where you want to switch In-Context on or off based on user actions. ### [Tagging Only Visible Strings](#tagging-only-visible-strings) [Section titled “Tagging Only Visible Strings”](#tagging-only-visible-strings) ```js _jipt.push(['tag_only_visible_phrases', true]); ``` When set to `true`, this parameter ensures the screenshot tagging feature only processes strings currently visible in the user’s viewport. This is ideal for dynamic interfaces, like single-page applications that use modals or dialogs that cover other on-page content. Enabling this option prevents hidden strings from being tagged, resulting in cleaner screenshots and more accurate context for translators. Acceptable values: `true`, `false`. ## [Optional Functions](#optional-functions) [Section titled “Optional Functions”](#optional-functions) You can call various functions on the global `window.jipt` object to manage or customize In-Context in real time. ### [Start and Stop Methods](#start-and-stop-methods) [Section titled “Start and Stop Methods”](#start-and-stop-methods) ```js window.jipt.start(); ``` Manually activates In-Context for the current page. Use this if you set `start_type` to `manual` or if you want to re-activate In-Context after stopping it. ```js window.jipt.stop(); ``` Stops the In-Context overlay. All interactive translation elements are removed until you call `start()` again. **Usage Example: Single-Page Application** In a single-page application, you might switch In-Context on or off when a user changes the language: ```js const handleLanguageChange = (event) => { const language = event.target.value; setSelectedLanguage(language); if (language === inContextLanguage) { window.jipt.start(); } else { window.jipt.stop(); } }; ``` In the above snippet, whenever the user selects the In-Context pseudo-language, In-Context is triggered. When they select any other language, In-Context is stopped. ## [Managing Screenshots via In-Context](#managing-screenshots-via-in-context) [Section titled “Managing Screenshots via In-Context”](#managing-screenshots-via-in-context) With Crowdin In-Context, you can manage screenshots of your web application pages directly while browsing them. This feature helps you provide up-to-date visual context for translators, ensuring string tagging stays accurate and relevant. Screenshots taken via In-Context are automatically uploaded to your Crowdin project. All visible strings on the page are tagged automatically, simplifying context management for your translation team. Read more about [Screenshots](/screenshots/). [Automated Screenshot Management ](/developer/automating-screenshot-management/)Learn how to automate screenshot creation and updates. ### [Adding Screenshots via In-Context](#adding-screenshots-via-in-context) [Section titled “Adding Screenshots via In-Context”](#adding-screenshots-via-in-context) You can take a screenshot of any page of your website while using In-Context and upload it to your Crowdin project. Crowdin will automatically tag all strings displayed on the page in the screenshot. To add a screenshot via In-Context, follow these steps: 1. Open your website page with In-Context enabled. 2. In the Crowdin In-Context dialog, switch to the **Screenshot** tab. 3. In the **Name** field, check the automatically suggested screenshot name based on the page title. Edit it if needed. If no screenshots were previously added for this page, the **Replace existing screenshot** drop-down will display *There is no screenshot with this name*. 4. Click **Add Screenshot**.  Crowdin will upload the screenshot to your project and automatically tag all strings displayed on the page. ### [Updating Screenshots via In-Context](#updating-screenshots-via-in-context) [Section titled “Updating Screenshots via In-Context”](#updating-screenshots-via-in-context) You can manually update existing screenshots in your Crowdin project directly from In-Context. This helps keep screenshots and string tags current when your web pages are updated with new content or layout changes. To update an existing screenshot via In-Context, follow these steps: 1. Open the updated page of your website with In-Context enabled. 2. In the Crowdin In-Context dialog, switch to the **Screenshot** tab. 3. In the **Name** field, check the automatically suggested screenshot name based on the page title. 4. In the **Replace existing screenshot** drop-down, select the screenshot you want to replace. You can also view the time and date the existing screenshot was added to confirm you’re replacing the correct one. 5. Click **Replace Screenshot**.  Crowdin will replace the previous screenshot in your project with the new one and automatically tag all strings displayed on the page. ## [Troubleshooting and Common Questions](#troubleshooting-and-common-questions) [Section titled “Troubleshooting and Common Questions”](#troubleshooting-and-common-questions)
# IP Addresses and Domains
> Explore the IP addresses and domains that Crowdin uses to interact with its services
When you work with private integrations (e.g., integrations with self-hosted VCS) or use a firewall for your company network, you need to add dedicated Crowdin IP addresses and domains to the allowlist to ensure that it operates properly while staying secure. A similar approach is applicable when using Crowdin webhooks. Caution The IP addresses listed below change periodically. ## [Integrations and Applications](#integrations-and-applications) [Section titled “Integrations and Applications”](#integrations-and-applications) Crowdin uses the following IP address to interact with the integrations and Crowdin Apps: ```shell 52.45.158.111/32 34.232.97.56/32 54.164.39.118/32 ``` ## [Webhooks, AI Providers, and MT Engines](#webhooks-ai-providers-and-mt-engines) [Section titled “Webhooks, AI Providers, and MT Engines”](#webhooks-ai-providers-and-mt-engines) Crowdin uses the following IP addresses to send webhooks and interact with AI providers and MT engines: ```shell 3.88.119.237/32 3.224.117.103/32 3.255.22.171/32 13.223.127.35/32 18.204.99.167/32 35.171.66.7/32 44.194.158.59/32 44.196.180.134/32 44.215.240.56/32 52.6.250.28/32 52.55.92.57/32 52.72.249.65/32 52.73.138.157/32 52.86.187.19/32 54.85.180.8/32 54.235.202.61/32 ``` ## [Downloading Lists of Current IP Addresses](#downloading-lists-of-current-ip-addresses) [Section titled “Downloading Lists of Current IP Addresses”](#downloading-lists-of-current-ip-addresses) For your convenience, you can get the [current IP addresses](/ips/ips.json) in JSON format. ## [Domains](#domains) [Section titled “Domains”](#domains) Crowdin uses the following domains to provide its services from: ```shell accounts.crowdin.com crowdin.com api.crowdin.com crowdin-assets.cf-downloads.crowdin.com crowdin-attachments.cf-downloads.crowdin.com crowdin-importer.cf-downloads.crowdin.com crowdin-packages.cf-downloads.crowdin.com crowdin-screenshots.cf-downloads.crowdin.com crowdin-static.cf-downloads.crowdin.com crowdin-tmp.cf-downloads.crowdin.com statics.crowdin.net distributions.crowdin.net badges.crowdin.net ws-lb.crowdin.com cdn.crowdin.com d2srrzh48sp2nh.cloudfront.net enterprise.crowdin.com enterprise-statics.crowdin.net .crowdin.com .api.crowdin.com production-enterprise-assets.cf-downloads.crowdin.com production-enterprise-attachments.cf-downloads.crowdin.com production-enterprise-importer.cf-downloads.crowdin.com production-enterprise-packages.cf-downloads.crowdin.com production-enterprise-screenshots.cf-downloads.crowdin.com production-enterprise-static.cf-downloads.crowdin.com production-enterprise-tmp.cf-downloads.crowdin.com enterprise-euw1-statics.crowdin.net badges-euw1.crowdin.net enterprise-euw1.crowdin.com production-euw1-enterprise-assets.cf-downloads.crowdin.com production-euw1-enterprise-attachments.cf-downloads.crowdin.com production-euw1-enterprise-importer.cf-downloads.crowdin.com production-euw1-enterprise-packages.cf-downloads.crowdin.com production-euw1-enterprise-screenshots.cf-downloads.crowdin.com production-euw1-enterprise-static.cf-downloads.crowdin.com production-euw1-enterprise-tmp.cf-downloads.crowdin.com ```
# Language Codes
> Explore the list of language codes supported by Crowdin and Crowdin Enterprise
* Crowdin Code: \[ ] Two-letters \[ ] Three-letters \[ ] Locale \[ ] Android \[ ] OSX \[ ] OSX Locale | Code | Name | Two-letters | Three-letters | Locale | Android | OSX | OSX Locale | | ---- | ---- | ----------- | ------------- | ------ | ------- | --- | ---------- | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | * Crowdin Enterprise Code: \[ ] Two-letters \[ ] Three-letters \[ ] Locale \[ ] Android \[ ] OSX \[ ] OSX Locale | Code | Name | Two-letters | Three-letters | Locale | Android | OSX | OSX Locale | | ---- | ---- | ----------- | ------------- | ------ | ------- | --- | ---------- | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
# Pseudolocalization
> Check how your product's UI will look after translation
Pseudo-localization is a method used to check the software’s readiness to be localized. This method shows how the product’s UI will look after translation. Use this feature to reduce potential rework by checking whether any source strings should be altered before the translation process begins. With Pseudo-localization, you can quickly check the following: * How languages with texts that tend to be longer/shorter than the source texts fit your product’s UI * Check whether all the translatable strings are uploaded into your project * Find non-translatable texts in your software (might be strings like application name) * See how well your product supports different character sets and right-to-left/left-to-right languages ## [Parameters](#parameters) [Section titled “Parameters”](#parameters) Pseudo-localization can be customized to fit your needs. There are several parameters you can adjust to see how your application will look with different languages. ### [Length Correction](#length-correction) [Section titled “Length Correction”](#length-correction) Allows you make strings longer or shorter to see if your product’s UI handles other languages correctly. Because translations in some languages may be longer or shorter than the source texts in your project. For example, Spanish texts are on average 25-30% longer than English texts, while Japanese texts are 30-60% shorter. ### [Prefix/Suffix Adding](#prefixsuffix-adding) [Section titled “Prefix/Suffix Adding”](#prefixsuffix-adding) Add prefixes and suffixes to see where each string starts and ends in the UI, regardless of the language. ### [Character Transformation](#character-transformation) [Section titled “Character Transformation”](#character-transformation) Character Transformation replaces English characters with easily recognizable accented versions, random Arabic symbols, or Chinese ideographs to make it obvious if there are some hard-coded strings in your application. This ensures that your application is ready for localization into other languages (including right-to-left and left-to-right languages). ## [Pseudo-Localization App](#pseudo-localization-app) [Section titled “Pseudo-Localization App”](#pseudo-localization-app) To run Pseudo-localization, you can use the Pseudo-localization app that could be installed via Crowdin Store. [Install the App ](https://store.crowdin.com/pseudolocalization) To set up the Pseudo-localization app, follow these steps: 1. Select one of the available presets or set all the required parameters manually. 2. Click **Pseudo-localize and Download**. As a result, you will get an archive with pseudo-localized project files. 3. Integrate the downloaded files into your software. ## [Pseudo-Localization via CLI](#pseudo-localization-via-cli) [Section titled “Pseudo-Localization via CLI”](#pseudo-localization-via-cli) Another way to set up and download your pseudo-localized project files is to use Crowdin CLI. Read more about [Downloading Pseudo-localization via CLI](https://crowdin.github.io/crowdin-cli/advanced#download-pseudo-localization). ## [Pseudo-Localization via API](#pseudo-localization-via-api) [Section titled “Pseudo-Localization via API”](#pseudo-localization-via-api) If you prefer working with API, you can use the following API methods. * Crowdin * [Build Project Translation](/developer/api/v2/#operation/api.projects.translations.builds.post) - ensure to switch to the `TranslationCreateProjectPseudoBuildForm` schema. * [Check Project Build Status](/developer/api/v2/#operation/api.projects.translations.builds.get). * [Download Project Translations](/developer/api/v2/#operation/api.projects.translations.builds.download.download). * Crowdin Enterprise * [Build Project Translation](/developer/enterprise/api/v2/#operation/api.projects.translations.builds.post) - ensure to switch to the `TranslationCreateProjectPseudoBuildForm` schema. * [Check Project Build Status](/developer/enterprise/api/v2/#operation/api.projects.translations.builds.get). * [Download Project Translations](/developer/enterprise/api/v2/#operation/api.projects.translations.builds.download.download). This is an asynchronous API, so you must check the build status before downloading the pseudo-localized files. Read more about [API](/developer/api/). ## [Pseudo-Localization via Design Plugins](#pseudo-localization-via-design-plugins) [Section titled “Pseudo-Localization via Design Plugins”](#pseudo-localization-via-design-plugins) For design tool (Figma, Adobe XD) users, there’s also an option to test whether the designs are ready to be localized using pseudo-localization. This feature is integrated into Crowdin for Figma and Crowdin for Adobe XD plugins. It allows you to simulate how your content (e.g., the application’s UI) will look with different languages to check whether the source strings should be modified before the project localization starts. You can start pseudo-localizing your content right after sending your texts to Crowdin. [Pseudo-localization via Crowdin for Figma plugin ](/figma-plugin/#pseudo-localization) [Pseudo-localization via Crowdin for Adobe XD plugin ](/adobe-xd-plugin/#pseudo-localization)
# Shopify Apps Localization
> Welcome Shopify Partner! If you're looking to expand your app's reach to new markets and users, and enhance your existing global user experience, this guide will help you translate your Shopify app using Crowdin.
 ## [Overview](#overview) [Section titled “Overview”](#overview) The translation process itself is simple with Crowdin. It involves integrating your Github (or any other GIT server) repository with Crowdin. Then your app’s language resource files are imported into Crowdin, where they can be translated into different languages. Once the translations are complete, you can easily export them back to your Github repo. Most importantly, if you have updates in your app, Crowdin will pick the changes up and allow you to easily translate the new content. Professional Translations When you set up your Crowdin project, both source texts flow to Crowdin and translated content flows back. You can choose a translation vendor to buy translations from. As a Shopify partner, you may be eligible for Shopify to cover the cost of localizing your app. Be sure to contact before purchasing translations. To be eligible for reimbursement, you should buy translations from the following translation vendors: * [BLEND](https://store.crowdin.com/partners/blend) * [Translated](https://store.crowdin.com/partners/translated) AI Translations  Crowdin has a wide range of integrations with AI Translation tools and machine translation engines. If you decide to use AI to translate some of your content, check out the appropriate section of the [Crowdin Store](https://store.crowdin.com/categories/ai). Read more about [Crowdin AI](https://crowdin.com/ai-localization) and how it can help you translate your Shopify app. ### ✅ Doing Multilingual Business As you localize your application, more parts of your operations will likely become multilingual. For example, emails you send or help pages you offer to customers. Crowdin will serve as your LangOps tool for your global operations. If you need to translate marketing content, knowledgebase content, or other materials, be sure to check out the [Crowdin Store](https://store.crowdin.com) for integration with your marketing automation system or help desk. ### ✅ Continuous Localization If you’re constantly improving your Shopify app, you may have multiple branches in your repo. All branches can be synchronized to Crowdin and translated in parallel with the development process, ensuring that localization doesn’t delay your release. ### ✅ Context and Quality Translations Since resource files usually don’t contain enough contextual information for linguists, it is always recommended to provide textual information for your keys (UI copy), upload screenshots of your app, and be sure to respond to linguists when they request context when translating your project. ## [For Developers: Steps to Integrate GitHub with Crowdin](#integration) [Section titled “For Developers: Steps to Integrate GitHub with Crowdin”](#integration) Here’s a step-by-step guide on how to start translating your Shopify app with Crowdin: 1. **Create a Crowdin Account**. Create a Free Crowdin account [here](https://crowdin.com/pricing) (Pick the Free plan). 2. **Create a New Project**. After signing up, go to your profile page and click [Create Project](https://crowdin.com/createproject). Give the project a name, select your source and target languages. 3. **Connect Your GitHub Repository**. Go to your project settings, click on **Integrations**, and select **GitHub**.\ You’ll need to authenticate your GitHub account and authorize Crowdin. Visit this page to learn more about [Crowdin’s GitHub integration](https://store.crowdin.com/github). If you use different code hosting, here you can find all the possible integration tools including integrations with other Git services and CLI tool for more advanced setups. 4. **Start Translating**. Your strings will be imported into Crowdin and you can now begin the translation process. ## [Buying Translations](#buying-translations) [Section titled “Buying Translations”](#buying-translations) 1. **Navigate to Your Project**. Navigate to your app’s Crowdin page. 2. **Go to Tasks**. From the top navigation bar, click on **Tasks**. This will lead you to the Tasks Management page. 3. **Connect Your Github Repository**. Click the **Create Task** button, usually located at the upper-right corner of the screen. 4. **Define Task Parameters**. A form will appear where you’ll need to fill out details about the task: * **Languages:** Choose the target languages for this task. * **Type of Activity:** Choose whether the task involves translation, proofreading, or both. * **Files to Translate:** Select the specific files that require translation. * **Assignee:** Select the translation agency of your choise. * **Deadline:** If necessary, set a due date for the task. 5. **Create the Task**. Once you’ve filled out the necessary details, click the **Create** button. Getting Help Crowdin provides free, 24x7 technical support to Shopify partners and can help you get your Shopify app global. [Contact Us ](https://crowdin.com/contacts)
# Understanding Scopes
> Learn about the scopes available in Crowdin API
Scopes let you set the exact access type you need. Scopes limit access for the personal access tokens, OAuth tokens, and Crowdin apps. They don’t provide any additional permissions except those the user already has. The scopes you specify for the OAuth app on Crowdin will be shown to the users on the authorization form. ## [Available scopes](#available-scopes) [Section titled “Available scopes”](#available-scopes) | Name | Key | Access Level | Description | | --------------------------- | --------------------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | All scopes | `*` | Read and Write, Read only | Grants access to perform any of the following actions. | | Notifications | `notification` | Read and Write, Read only | Grants access to the list of enabled notifications, the ability to subscribe to a channel, as well as unsubscribe from it. | | Languages | `language` | Read and Write, Read only | Grants access to manage organization languages. | | Applications | `application` | Read and Write, Read only | Grants access to install applications. | | Translation memories | `tm` | Read and Write, Read only | Grants access to manage project Translation Memory files. | | Machine translation engines | `mt` | Read and Write, Read only | Grants access to get the list of connected Machine Translation (MT) engines, create MT engines, delete MT engines, and update their settings. | | Webhooks | `webhook` | Read and Write, Read only | Grants access to manage organization webhooks. | | Vendors | `vendor` | Read only | Crowdin Enterprise only. Grants access to get organization vendors list. | | Clients | `client` | Read only | Crowdin Enterprise only. Grants access to get organization clients list. | | Glossaries | `glossary` | Read and Write, Read only | Grants access to manage the files with the project terminology. | | Users | `user` | Read and Write, Read only | Crowdin Enterprise only. Grants access to manage users. | | Teams | `team` | Read and Write, Read only | Crowdin Enterprise only. Grants access to teams of users. | | Fields | `field` | Read and Write, Read only | Crowdin Enterprise only. Grants access to organization fields, permission to create, delete, and update field settings. | | Groups | `group` | Read and Write, Read only | Crowdin Enterprise only. Grants access to manage project groups a user has access to. | | AI | `ai` | Read and Write, Read only | Grants access to manage AI providers, prompts, fine-tuning, and request proxying to AI providers. | | AI Providers | `ai.provider` | Read and Write, Read only | Grants access to manage AI providers. | | AI Prompts | `ai.prompt` | Read and Write, Read only | Grants access to manage AI prompts. | | AI Proxy | `ai.proxy` | Read only | Enables request proxying to AI providers. | | AI Fine-tuning | `ai.fine-tuning` | Read and Write, Read only | Grants access to manage the fine-tuning of AI prompts. | | Automations | `automation` | Read and Write, Read only | Crowdin Enterprise only. Grants access to manage automation rules and rule executions. | | Automation rules | `automation.rule` | Read and Write, Read only | Crowdin Enterprise only. Grants access to manage automation rules. | | Automation rule executions | `automation.rule.execution` | Read and Write, Read only | Crowdin Enterprise only. Grants access to manage automation rule executions. | | Projects | `project` | Read and Write, Read only | Grants access to manage projects a user has access to. | | Projects (Settings) | `project.settings` | Read and Write, Read only | Grants access to project lists, permission to view, create, and update project settings. | | Members and teams | `project.member` | Read and Write, Read only | Grants access to member and team lists, permission to view, invite, and manage permissions for project members and teams. | | Tasks | `project.task` | Read and Write, Read only | Grants access to task lists, permission to create, delete, and update project tasks. | | Reports | `project.report` | Read and Write, Read only | Grants access to reports list, permission to generate, and export project reports. | | Translation status | `project.status` | Read only | Grants access to translation status for the projects: current translation issues, translation progress on different levels (file, language, branch, directory). | | Source files & strings | `project.source` | Read and Write, Read only | Grants access to add, get, delete, and update project branches, directories, source files, and source strings, as well as access to file revisions. | | Webhooks | `project.webhook` | Read and Write, Read only | Grants access to read or write hook configurations on projects a user manages. | | Translations | `project.translation` | Read and Write, Read only | Grants access to add new and manage existing translations. | | Screenshots | `project.screenshot` | Read and Write, Read only | Grants access to get screenshots list, add, get, replace, and delete screenshots, ability to access and modify screenshot tags. | | Security logs | `security-log` | Read only | Grants access to get security logs list and get security log item. | | Organization | `organization` | Read only | Crowdin Enterprise only. Grants access to get organization info, account defaults, and authentication settings. | | Custom Spellcheckers | `custom-spellchecker` | Read only | Crowdin Enterprise only. Grants access to get the custom spellcheckers list and individual spellchecker items. | | External QA Checks | `external-qa-check` | Read only | Crowdin Enterprise only. Grants access to get organization external qa checks. | Caution The Vendors, Clients, Translation status, AI Proxy, Security logs, Organization, Custom Spellcheckers, and External QA Checks scopes are read-only by default and cannot be edited.
# Webhook Events
> Explore the list of events and payload examples
You can add webhooks to build integrations with the third-party services or with your backend. After you configure a webhook for the project, Crowdin will start sending POST or GET requests with data to the webhook URL via HTTP. ## [Configuring Webhooks](#configuring-webhooks) [Section titled “Configuring Webhooks”](#configuring-webhooks) Webhook integration can be implemented at different levels, including Project, Account, or Organization level. Project Configure webhooks for a specific project to receive notifications about events in the project such as file translation, review, and more. See [Crowdin Webhooks](/webhooks/) and [Crowdin Enterprise Webhooks](/enterprise/webhooks/) for more details. Organization Configure webhooks for your Crowdin Enterprise organization to receive notifications when projects and groups are created or deleted. See [Organization Settings](/enterprise/organization-settings/#webhooks) for more details. Account Configure webhooks for your Crowdin account to receive notifications when projects are created or deleted. See [Account Settings](/account-settings/#webhooks) for more details. Depending on your approach to webhooks management, you might need to add dedicated Crowdin IP addresses to your firewall to allow Crowdin to open the pre-configured webhook URLs. Read more about [IP Addresses](/developer/ip-addresses/#webhooks-ai-providers-and-mt-engines). ## [Events](#events) [Section titled “Events”](#events) You can configure webhooks for different events that occur in the project, account, or organization. #### [File](#file) [Section titled “File”](#file) * [File fully translated](#file-fully-translated) – a file is translated into one of the target languages * [File fully reviewed](#file-fully-reviewed) – translations in a file are approved for one of the target languages * [File added](#file-added) – a new file is added to the project * [File updated](#file-updated) – a file is updated * [File reverted](#file-reverted) – a file is reverted to the previous revision * [File deleted](#file-deleted) – a file is deleted #### [Project](#project) [Section titled “Project”](#project) * [Project fully translated](#project-fully-translated) – all files are translated into one of the target languages * [Project fully reviewed](#project-fully-reviewed) – translations in all files are approved for one of the target languages * [Project successfully built](#project-successfully-built) – a project is built successfully * [Exported translation updated](#exported-translation-updated) – final translation of a string is updated #### [Source String](#source-string) [Section titled “Source String”](#source-string) * [Source string added](#source-string-added) – a new string is added to the project * [Source string updated](#source-string-updated) – a string in the project is updated * [Source string deleted](#source-string-deleted) – a string is deleted #### [Suggested Translation](#suggested-translation) [Section titled “Suggested Translation”](#suggested-translation) * [Suggested translation added](#suggested-translation-added) – a string in the project is translated * [Suggested translation updated](#suggested-translation-updated) – a translation for a string in the project is updated * [Suggested translation deleted](#suggested-translation-deleted) – one of the translations is deleted * [Suggested translation approved](#suggested-translation-approved) – a translation for a string is approved * [Suggested translation disapproved](#suggested-translation-disapproved) – approval for a previously added translation is removed #### [String Comment/Issue](#string-commentissue) [Section titled “String Comment/Issue”](#string-commentissue) * [String comment/issue created](#string-commentissue-created) – a comment or issue is added to the string * [String comment/issue updated](#string-commentissue-updated) – a comment or issue is updated * [String comment/issue deleted](#string-commentissue-deleted) – a comment or issue is deleted * [String comment/issue restored](#string-commentissue-restored) – a comment or issue is restored #### [Task](#task) [Section titled “Task”](#task) * [Task added](#task-added) – a task is added to the project * [Task status changed](#task-status-changed) – a task status is changed * [Task updated](#task-updated) – a task is updated * [Task deleted](#task-deleted) – a task is deleted #### [Account and Organization](#account-and-organization) [Section titled “Account and Organization”](#account-and-organization) * [Project created](#project-created) – a project is created * [Project deleted](#project-deleted) – a project is deleted * [Group created](#group-created) – a group is created (Crowdin Enterprise only) * [Group deleted](#group-deleted) – a group is deleted (Crowdin Enterprise only) ## [Webhook Payload Examples](#webhook-payload-examples) [Section titled “Webhook Payload Examples”](#webhook-payload-examples) View the examples of the webhook payloads for different events. ### [File Fully Translated](#file-fully-translated) [Section titled “File Fully Translated”](#file-fully-translated) ```json { "event": "file.translated", "file": { "id": "44", "name": "umbrella_app.xliff", "title": "source_app_info", "type": "xliff", "path": "/directory1/directory2/filename.extension", "status": "active", "revision": "1", "branchId": "34", "directoryId": "4", "project": { "id": "777", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://crowdin.com/project/umbrella", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null } } ``` ### [File Fully Reviewed](#file-fully-reviewed) [Section titled “File Fully Reviewed”](#file-fully-reviewed) ```json { "event": "file.approved", "file": { "id": "44", "name": "umbrella_app.xliff", "title": "source_app_info", "type": "xliff", "path": "/directory1/directory2/filename.extension", "status": "active", "revision": "1", "branchId": "34", "directoryId": "4", "project": { "id": "777", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://crowdin.com/project/umbrella", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null } } ``` ### [File Added](#file-added) [Section titled “File Added”](#file-added) ```json { "event": "file.added", "file": { "id": "44", "name": "umbrella_app.xliff", "title": "source_app_info", "type": "xliff", "path": "/directory1/directory2/filename.extension", "status": "active", "revision": "1", "branchId": "34", "directoryId": "4", "project": { "id": "777", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://crowdin.com/project/umbrella", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } }, "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" } } ``` ### [File Updated](#file-updated) [Section titled “File Updated”](#file-updated) ```json { "event": "file.updated", "file": { "id": "44", "name": "umbrella_app.xliff", "title": "source_app_info", "type": "xliff", "path": "/directory1/directory2/filename.extension", "status": "active", "revision": "1", "branchId": "34", "directoryId": "4", "project": { "id": "777", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://crowdin.com/project/umbrella", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } }, "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" } } ``` ### [File Reverted](#file-reverted) [Section titled “File Reverted”](#file-reverted) ```json { "event": "file.reverted", "file": { "id": "44", "name": "umbrella_app.xliff", "title": "source_app_info", "type": "xliff", "path": "/directory1/directory2/filename.extension", "status": "active", "revision": "1", "branchId": "34", "directoryId": "4", "project": { "id": "777", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://crowdin.com/project/umbrella", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } }, "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" } } ``` ### [File Deleted](#file-deleted) [Section titled “File Deleted”](#file-deleted) ```json { "event": "file.deleted", "file": { "id": "44", "name": "umbrella_app.xliff", "title": "source_app_info", "type": "xliff", "path": "/directory1/directory2/filename.extension", "status": "active", "revision": "1", "branchId": "34", "directoryId": "4", "project": { "id": "777", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://crowdin.com/project/umbrella", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } }, "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" } } ``` ### [Project Fully Translated](#project-fully-translated) [Section titled “Project Fully Translated”](#project-fully-translated) ```json { "event": "project.translated", "project": { "id": "777", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://crowdin.com/project/umbrella", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null } } ``` ### [Project Fully Reviewed](#project-fully-reviewed) [Section titled “Project Fully Reviewed”](#project-fully-reviewed) ```json { "event": "project.approved", "project": { "id": "777", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://crowdin.com/project/umbrella", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null } } ``` ### [Project Successfully Built](#project-successfully-built) [Section titled “Project Successfully Built”](#project-successfully-built) ```json { "event": "project.built", "build": { "id": "1", "downloadUrl": "https://example.crowdin.com/api/v2/projects/777/translations/builds/1/download", "project": { "id": "777", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://crowdin.com/project/umbrella", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } } } ``` ### [Exported Translation Updated](#exported-translation-updated) [Section titled “Exported Translation Updated”](#exported-translation-updated) * File-based project ```json { "event": "translation.updated", "oldTranslation": { "id": "1", "text": "1", "pluralCategoryName": "1", "rating": "10", "createdAt": "2022-05-05T11:26:54+00:00" }, "newTranslation": { "id": "1", "text": "1", "pluralCategoryName": "1", "rating": "10", "createdAt": "2022-05-05T11:26:54+00:00", "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "string": { "id": "2814", "identifier": "name", "text": "Not all videos are shown to users. See more", "type": "text", "context": "shown on main page", "maxLength": "35", "isHidden": false, "isDuplicate": true, "masterStringId": "1", "revision": "1", "hasPlurals": false, "labelIds": [ 3, 8 ], "url": "https://example.crowdin.com/translate/file-format-samples/1/en-uk/78#1", "createdAt": "2022-04-20T12:43:57+00:00", "updatedAt": "2022-04-20T13:24:01+00:00", "file": { "id": "44", "name": "umbrella_app.xliff", "title": "source_app_info", "type": "xliff", "path": "/directory1/directory2/filename.extension", "status": "active", "revision": "1", "branchId": "34", "directoryId": "4" }, "project": { "id": "777", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://crowdin.com/project/umbrella", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } } } } ``` * String-based project ```json { "event": "translation.updated", "oldTranslation": { "id": "1", "text": "1", "pluralCategoryName": "1", "rating": "10", "provider": null, "isPreTranslated": false, "createdAt": "2022-05-05T11:26:54+00:00" }, "newTranslation": { "id": "1", "text": "1", "pluralCategoryName": "1", "rating": "10", "provider": null, "isPreTranslated": false, "createdAt": "2022-05-05T11:26:54+00:00", "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "string": { "id": "2814", "identifier": "b068931cc450442b63f5b3d276ea4297", "key": "name", "text": "Not all videos are shown to users. See more", "type": "text", "context": "shown on main page", "maxLength": "35", "isHidden": false, "isDuplicate": true, "masterStringId": "1", "hasPlurals": false, "labelIds": [ 3, 8 ], "url": "https://example.crowdin.com/translate/umbrella-sb/1/en-uk/78#1", "createdAt": "2022-04-20T12:43:57+00:00", "updatedAt": "2022-04-20T13:24:01+00:00", "branch": { "id": "34" }, "project": { "id": "627210", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella-sb", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://example.crowdin.com/u/projects/627210", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } } } } ``` ### [Source String Added](#source-string-added) [Section titled “Source String Added”](#source-string-added) * File-based project ```json { "events": [ { "event": "string.added", "string": { "id": "2814", "identifier": "name", "text": "Not all videos are shown to users. See more", "type": "text", "context": "shown on main page", "maxLength": "35", "isHidden": false, "isDuplicate": true, "masterStringId": "1", "revision": "1", "hasPlurals": false, "labelIds": [ 3, 8 ], "url": "https://example.crowdin.com/translate/file-format-samples/1/en-uk/78#1", "createdAt": "2022-04-20T12:43:57+00:00", "updatedAt": "2022-04-20T13:24:01+00:00", "file": { "id": "44", "name": "umbrella_app.xliff", "title": "source_app_info", "type": "xliff", "path": "/directory1/directory2/filename.extension", "status": "active", "revision": "1", "branchId": "34", "directoryId": "4" }, "project": { "id": "777", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://crowdin.com/project/umbrella", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } }, "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" } } ] } ``` * String-based project ```json { "events": [ { "event": "string.added", "string": { "id": "2814", "identifier": "b068931cc450442b63f5b3d276ea4297", "key": "name", "text": "Not all videos are shown to users. See more", "type": "text", "context": "shown on main page", "maxLength": "35", "isHidden": false, "isDuplicate": true, "masterStringId": "1", "hasPlurals": false, "labelIds": [ 3, 8 ], "url": "https://example.crowdin.com/translate/umbrella-sb/1/en-uk/78#1", "createdAt": "2022-04-20T12:43:57+00:00", "updatedAt": "2022-04-20T13:24:01+00:00", "branch": { "id": "34" }, "project": { "id": "627210", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella-sb", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://example.crowdin.com/u/projects/627210", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } }, "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" } } ] } ``` ### [Source String Updated](#source-string-updated) [Section titled “Source String Updated”](#source-string-updated) * File-based project ```json { "events": [ { "event": "string.updated", "string": { "id": "2814", "identifier": "name", "text": "Not all videos are shown to users. See more", "type": "text", "context": "shown on main page", "maxLength": "35", "isHidden": false, "isDuplicate": true, "masterStringId": "1", "revision": "1", "hasPlurals": false, "labelIds": [ 3, 8 ], "url": "https://example.crowdin.com/translate/file-format-samples/1/en-uk/78#1", "createdAt": "2022-04-20T12:43:57+00:00", "updatedAt": "2022-04-20T13:24:01+00:00", "file": { "id": "44", "name": "umbrella_app.xliff", "title": "source_app_info", "type": "xliff", "path": "/directory1/directory2/filename.extension", "status": "active", "revision": "1", "branchId": "34", "directoryId": "4" }, "project": { "id": "777", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://crowdin.com/project/umbrella", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } }, "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" } } ] } ``` * String-based project ```json { "events": [ { "event": "string.updated", "string": { "id": "2814", "identifier": "b068931cc450442b63f5b3d276ea4297", "key": "name", "text": "Not all videos are shown to users. See more", "type": "text", "context": "shown on main page", "maxLength": "35", "isHidden": false, "isDuplicate": true, "masterStringId": "1", "hasPlurals": false, "labelIds": [ 3, 8 ], "url": "https://example.crowdin.com/translate/umbrella-sb/1/en-uk/78#1", "createdAt": "2022-04-20T12:43:57+00:00", "updatedAt": "2022-04-20T13:24:01+00:00", "branch": { "id": "34" }, "project": { "id": "627210", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella-sb", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://example.crowdin.com/u/projects/627210", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } }, "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" } } ] } ``` ### [Source String Deleted](#source-string-deleted) [Section titled “Source String Deleted”](#source-string-deleted) * File-based project ```json { "events": [ { "event": "string.deleted", "string": { "id": "2814", "identifier": "name", "text": "Not all videos are shown to users. See more", "type": "text", "context": "shown on main page", "maxLength": "35", "isHidden": false, "isDuplicate": true, "masterStringId": "1", "revision": "1", "hasPlurals": false, "labelIds": [ 3, 8 ], "url": "https://example.crowdin.com/translate/file-format-samples/1/en-uk/78#1", "createdAt": "2022-04-20T12:43:57+00:00", "updatedAt": "2022-04-20T13:24:01+00:00", "file": { "id": "44", "name": "umbrella_app.xliff", "title": "source_app_info", "type": "xliff", "path": "/directory1/directory2/filename.extension", "status": "active", "revision": "1", "branchId": "34", "directoryId": "4" }, "project": { "id": "777", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://crowdin.com/project/umbrella", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } }, "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" } } ] } ``` * String-based project ```json { "events": [ { "event": "string.deleted", "string": { "id": "2814", "identifier": "b068931cc450442b63f5b3d276ea4297", "key": "name", "text": "Not all videos are shown to users. See more", "type": "text", "context": "shown on main page", "maxLength": "35", "isHidden": false, "isDuplicate": true, "masterStringId": "1", "hasPlurals": false, "labelIds": [ 3, 8 ], "url": "https://example.crowdin.com/translate/umbrella-sb/1/en-uk/78#1", "createdAt": "2022-04-20T12:43:57+00:00", "updatedAt": "2022-04-20T13:24:01+00:00", "branch": { "id": "34" }, "project": { "id": "627210", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella-sb", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://example.crowdin.com/u/projects/627210", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } }, "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" } } ] } ``` ### [Suggested Translation Added](#suggested-translation-added) [Section titled “Suggested Translation Added”](#suggested-translation-added) * File-based project ```json { "event": "suggestion.added", "translation": { "id": "1", "text": "1", "pluralCategoryName": "1", "rating": "10", "createdAt": "2022-05-05T11:26:54+00:00", "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "string": { "id": "2814", "identifier": "name", "text": "Not all videos are shown to users. See more", "type": "text", "context": "shown on main page", "maxLength": "35", "isHidden": false, "isDuplicate": true, "masterStringId": "1", "revision": "1", "hasPlurals": false, "labelIds": [ 3, 8 ], "url": "https://example.crowdin.com/translate/file-format-samples/1/en-uk/78#1", "createdAt": "2022-04-20T12:43:57+00:00", "updatedAt": "2022-04-20T13:24:01+00:00", "file": { "id": "44", "name": "umbrella_app.xliff", "title": "source_app_info", "type": "xliff", "path": "/directory1/directory2/filename.extension", "status": "active", "revision": "1", "branchId": "34", "directoryId": "4" }, "project": { "id": "777", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://crowdin.com/project/umbrella", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } } } } ``` * String-based project ```json { "event": "suggestion.added", "translation": { "id": "1", "text": "1", "pluralCategoryName": "1", "rating": "10", "provider": null, "isPreTranslated": false, "createdAt": "2022-05-05T11:26:54+00:00", "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "string": { "id": "2814", "identifier": "b068931cc450442b63f5b3d276ea4297", "key": "name", "text": "Not all videos are shown to users. See more", "type": "text", "context": "shown on main page", "maxLength": "35", "isHidden": false, "isDuplicate": true, "masterStringId": "1", "hasPlurals": false, "labelIds": [ 3, 8 ], "url": "https://example.crowdin.com/translate/umbrella-sb/1/en-uk/78#1", "createdAt": "2022-04-20T12:43:57+00:00", "updatedAt": "2022-04-20T13:24:01+00:00", "branch": { "id": "34" }, "project": { "id": "627210", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella-sb", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://example.crowdin.com/u/projects/627210", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } } } } ``` ### [Suggested Translation Updated](#suggested-translation-updated) [Section titled “Suggested Translation Updated”](#suggested-translation-updated) * File-based project ```json { "event": "suggestion.updated", "translation": { "id": "1", "text": "1", "pluralCategoryName": "1", "rating": "10", "createdAt": "2022-05-05T11:26:54+00:00", "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "string": { "id": "2814", "identifier": "name", "text": "Not all videos are shown to users. See more", "type": "text", "context": "shown on main page", "maxLength": "35", "isHidden": false, "isDuplicate": true, "masterStringId": "1", "revision": "1", "hasPlurals": false, "labelIds": [ 3, 8 ], "url": "https://example.crowdin.com/translate/file-format-samples/1/en-uk/78#1", "createdAt": "2022-04-20T12:43:57+00:00", "updatedAt": "2022-04-20T13:24:01+00:00", "file": { "id": "44", "name": "umbrella_app.xliff", "title": "source_app_info", "type": "xliff", "path": "/directory1/directory2/filename.extension", "status": "active", "revision": "1", "branchId": "34", "directoryId": "4" }, "project": { "id": "777", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://crowdin.com/project/umbrella", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } } } } ``` * String-based project ```json { "event": "suggestion.updated", "translation": { "id": "1", "text": "1", "pluralCategoryName": "1", "rating": "10", "provider": null, "isPreTranslated": false, "createdAt": "2022-05-05T11:26:54+00:00", "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "string": { "id": "2814", "identifier": "b068931cc450442b63f5b3d276ea4297", "key": "name", "text": "Not all videos are shown to users. See more", "type": "text", "context": "shown on main page", "maxLength": "35", "isHidden": false, "isDuplicate": true, "masterStringId": "1", "hasPlurals": false, "labelIds": [ 3, 8 ], "url": "https://example.crowdin.com/translate/umbrella-sb/1/en-uk/78#1", "createdAt": "2022-04-20T12:43:57+00:00", "updatedAt": "2022-04-20T13:24:01+00:00", "branch": { "id": "34" }, "project": { "id": "627210", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella-sb", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://example.crowdin.com/u/projects/627210", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } } } } ``` ### [Suggested Translation Deleted](#suggested-translation-deleted) [Section titled “Suggested Translation Deleted”](#suggested-translation-deleted) * File-based project ```json { "event": "suggestion.deleted", "translation": { "id": "1", "text": "1", "pluralCategoryName": "1", "rating": "10", "createdAt": "2022-05-05T11:26:54+00:00", "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "string": { "id": "2814", "identifier": "name", "text": "Not all videos are shown to users. See more", "type": "text", "context": "shown on main page", "maxLength": "35", "isHidden": false, "isDuplicate": true, "masterStringId": "1", "revision": "1", "hasPlurals": false, "labelIds": [ 3, 8 ], "url": "https://example.crowdin.com/translate/file-format-samples/1/en-uk/78#1", "createdAt": "2022-04-20T12:43:57+00:00", "updatedAt": "2022-04-20T13:24:01+00:00", "file": { "id": "44", "name": "umbrella_app.xliff", "title": "source_app_info", "type": "xliff", "path": "/directory1/directory2/filename.extension", "status": "active", "revision": "1", "branchId": "34", "directoryId": "4" }, "project": { "id": "777", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://crowdin.com/project/umbrella", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } } } } ``` * String-based project ```json { "event": "suggestion.deleted", "translation": { "id": "1", "text": "1", "pluralCategoryName": "1", "rating": "10", "provider": null, "isPreTranslated": false, "createdAt": "2022-05-05T11:26:54+00:00", "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "string": { "id": "2814", "identifier": "b068931cc450442b63f5b3d276ea4297", "key": "name", "text": "Not all videos are shown to users. See more", "type": "text", "context": "shown on main page", "maxLength": "35", "isHidden": false, "isDuplicate": true, "masterStringId": "1", "hasPlurals": false, "labelIds": [ 3, 8 ], "url": "https://example.crowdin.com/translate/umbrella-sb/1/en-uk/78#1", "createdAt": "2022-04-20T12:43:57+00:00", "updatedAt": "2022-04-20T13:24:01+00:00", "branch": { "id": "34" }, "project": { "id": "627210", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella-sb", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://example.crowdin.com/u/projects/627210", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } } } } ``` ### [Suggested Translation Approved](#suggested-translation-approved) [Section titled “Suggested Translation Approved”](#suggested-translation-approved) * File-based project ```json { "event": "suggestion.approved", "translation": { "id": "1", "text": "1", "pluralCategoryName": "1", "rating": "10", "createdAt": "2022-05-05T11:26:54+00:00", "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "string": { "id": "2814", "identifier": "name", "text": "Not all videos are shown to users. See more", "type": "text", "context": "shown on main page", "maxLength": "35", "isHidden": false, "isDuplicate": true, "masterStringId": "1", "revision": "1", "hasPlurals": false, "labelIds": [ 3, 8 ], "url": "https://example.crowdin.com/translate/file-format-samples/1/en-uk/78#1", "createdAt": "2022-04-20T12:43:57+00:00", "updatedAt": "2022-04-20T13:24:01+00:00", "file": { "id": "44", "name": "umbrella_app.xliff", "title": "source_app_info", "type": "xliff", "path": "/directory1/directory2/filename.extension", "status": "active", "revision": "1", "branchId": "34", "directoryId": "4" }, "project": { "id": "777", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://crowdin.com/project/umbrella", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } } } } ``` * String-based project ```json { "event": "suggestion.approved", "translation": { "id": "1", "text": "1", "pluralCategoryName": "1", "rating": "10", "provider": null, "isPreTranslated": false, "createdAt": "2022-05-05T11:26:54+00:00", "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "string": { "id": "2814", "identifier": "b068931cc450442b63f5b3d276ea4297", "key": "name", "text": "Not all videos are shown to users. See more", "type": "text", "context": "shown on main page", "maxLength": "35", "isHidden": false, "isDuplicate": true, "masterStringId": "1", "hasPlurals": false, "labelIds": [ 3, 8 ], "url": "https://example.crowdin.com/translate/umbrella-sb/1/en-uk/78#1", "createdAt": "2022-04-20T12:43:57+00:00", "updatedAt": "2022-04-20T13:24:01+00:00", "branch": { "id": "34" }, "project": { "id": "627210", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella-sb", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://example.crowdin.com/u/projects/627210", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } } } } ``` ### [Suggested Translation Disapproved](#suggested-translation-disapproved) [Section titled “Suggested Translation Disapproved”](#suggested-translation-disapproved) * File-based project ```json { "event": "suggestion.disapproved", "translation": { "id": "1", "text": "1", "pluralCategoryName": "1", "rating": "10", "createdAt": "2022-05-05T11:26:54+00:00", "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "string": { "id": "2814", "identifier": "name", "text": "Not all videos are shown to users. See more", "type": "text", "context": "shown on main page", "maxLength": "35", "isHidden": false, "isDuplicate": true, "masterStringId": "1", "revision": "1", "hasPlurals": false, "labelIds": [ 3, 8 ], "url": "https://example.crowdin.com/translate/file-format-samples/1/en-uk/78#1", "createdAt": "2022-04-20T12:43:57+00:00", "updatedAt": "2022-04-20T13:24:01+00:00", "file": { "id": "44", "name": "umbrella_app.xliff", "title": "source_app_info", "type": "xliff", "path": "/directory1/directory2/filename.extension", "status": "active", "revision": "1", "branchId": "34", "directoryId": "4" }, "project": { "id": "777", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://crowdin.com/project/umbrella", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } } } } ``` * String-based project ```json { "event": "suggestion.disapproved", "translation": { "id": "1", "text": "1", "pluralCategoryName": "1", "rating": "10", "provider": null, "isPreTranslated": false, "createdAt": "2022-05-05T11:26:54+00:00", "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "string": { "id": "2814", "identifier": "b068931cc450442b63f5b3d276ea4297", "key": "name", "text": "Not all videos are shown to users. See more", "type": "text", "context": "shown on main page", "maxLength": "35", "isHidden": false, "isDuplicate": true, "masterStringId": "1", "hasPlurals": false, "labelIds": [ 3, 8 ], "url": "https://example.crowdin.com/translate/umbrella-sb/1/en-uk/78#1", "createdAt": "2022-04-20T12:43:57+00:00", "updatedAt": "2022-04-20T13:24:01+00:00", "branch": { "id": "34" }, "project": { "id": "627210", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella-sb", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://example.crowdin.com/u/projects/627210", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } } } } ``` ### [String Comment/Issue Created](#string-commentissue-created) [Section titled “String Comment/Issue Created”](#string-commentissue-created) * File-based project ```json { "event": "stringComment.created", "comment": { "id": "12", "text": "@BeMyEyes Please provide more details on where the text will be used", "type": "issue", "issueType": "source_mistake", "issueStatus": "unresolved", "resolvedAt": "2019-09-20T11:05:24+00:00", "createdAt": "2019-09-20T11:05:24+00:00", "string": { "id": "2814", "identifier": "b068931cc450442b63f5b3d276ea4297", "key": "name", "text": "Not all videos are shown to users. See more", "type": "text", "context": "shown on main page", "maxLength": "35", "isHidden": false, "isDuplicate": true, "masterStringId": "1", "revision": "1", "hasPlurals": false, "labelIds": [ 3, 8 ], "url": "https://example.crowdin.com/translate/file-format-samples/1/en-uk/78#1", "createdAt": "2022-04-20T12:43:57+00:00", "updatedAt": "2022-04-20T13:24:01+00:00", "file": { "id": "44", "name": "umbrella_app.xliff", "title": "source_app_info", "type": "xliff", "path": "/directory1/directory2/filename.extension", "status": "active", "revision": "1", "branchId": "34", "directoryId": "4" }, "project": { "id": "777", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://crowdin.com/project/umbrella", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" }, "commentResolver": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" }, "mention": { "userIds": [ 1 ] } } } ``` * String-based project ```json { "event": "stringComment.created", "comment": { "id": "12", "text": "@BeMyEyes Please provide more details on where the text will be used", "type": "issue", "issueType": "source_mistake", "issueStatus": "unresolved", "resolvedAt": "2019-09-20T11:05:24+00:00", "createdAt": "2019-09-20T11:05:24+00:00", "string": { "id": "2814", "identifier": "b068931cc450442b63f5b3d276ea4297", "key": "name", "text": "Not all videos are shown to users. See more", "type": "text", "context": "shown on main page", "maxLength": "35", "isHidden": false, "isDuplicate": true, "masterStringId": "1", "hasPlurals": false, "labelIds": [ 3, 8 ], "url": "https://example.crowdin.com/translate/umbrella-sb/1/en-uk/78#1", "createdAt": "2022-04-20T12:43:57+00:00", "updatedAt": "2022-04-20T13:24:01+00:00", "branch": { "id": "34" }, "project": { "id": "627210", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella-sb", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://example.crowdin.com/u/projects/627210", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" }, "commentResolver": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" }, "mention": { "userIds": [ 1 ] } } } ``` ### [String Comment/Issue Updated](#string-commentissue-updated) [Section titled “String Comment/Issue Updated”](#string-commentissue-updated) * File-based project ```json { "event": "stringComment.updated", "comment": { "id": "12", "text": "@BeMyEyes Please provide more details on where the text will be used", "type": "issue", "issueType": "source_mistake", "issueStatus": "unresolved", "resolvedAt": "2019-09-20T11:05:24+00:00", "createdAt": "2019-09-20T11:05:24+00:00", "string": { "id": "2814", "identifier": "b068931cc450442b63f5b3d276ea4297", "key": "name", "text": "Not all videos are shown to users. See more", "type": "text", "context": "shown on main page", "maxLength": "35", "isHidden": false, "isDuplicate": true, "masterStringId": "1", "revision": "1", "hasPlurals": false, "labelIds": [ 3, 8 ], "url": "https://example.crowdin.com/translate/file-format-samples/1/en-uk/78#1", "createdAt": "2022-04-20T12:43:57+00:00", "updatedAt": "2022-04-20T13:24:01+00:00", "file": { "id": "44", "name": "umbrella_app.xliff", "title": "source_app_info", "type": "xliff", "path": "/directory1/directory2/filename.extension", "status": "active", "revision": "1", "branchId": "34", "directoryId": "4" }, "project": { "id": "777", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://crowdin.com/project/umbrella", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" }, "commentResolver": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" }, "mention": { "userIds": [ 1 ] } } } ``` * String-based project ```json { "event": "stringComment.updated", "comment": { "id": "12", "text": "@BeMyEyes Please provide more details on where the text will be used", "type": "issue", "issueType": "source_mistake", "issueStatus": "unresolved", "resolvedAt": "2019-09-20T11:05:24+00:00", "createdAt": "2019-09-20T11:05:24+00:00", "string": { "id": "2814", "identifier": "b068931cc450442b63f5b3d276ea4297", "key": "name", "text": "Not all videos are shown to users. See more", "type": "text", "context": "shown on main page", "maxLength": "35", "isHidden": false, "isDuplicate": true, "masterStringId": "1", "hasPlurals": false, "labelIds": [ 3, 8 ], "url": "https://example.crowdin.com/translate/umbrella-sb/1/en-uk/78#1", "createdAt": "2022-04-20T12:43:57+00:00", "updatedAt": "2022-04-20T13:24:01+00:00", "branch": { "id": "34" }, "project": { "id": "627210", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella-sb", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://example.crowdin.com/u/projects/627210", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" }, "commentResolver": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" }, "mention": { "userIds": [ 1 ] } } } ``` ### [String Comment/Issue Deleted](#string-commentissue-deleted) [Section titled “String Comment/Issue Deleted”](#string-commentissue-deleted) * File-based project ```json { "event": "stringComment.deleted", "comment": { "id": "12", "text": "@BeMyEyes Please provide more details on where the text will be used", "type": "issue", "issueType": "source_mistake", "issueStatus": "unresolved", "resolvedAt": "2019-09-20T11:05:24+00:00", "createdAt": "2019-09-20T11:05:24+00:00", "string": { "id": "2814", "identifier": "b068931cc450442b63f5b3d276ea4297", "key": "name", "text": "Not all videos are shown to users. See more", "type": "text", "context": "shown on main page", "maxLength": "35", "isHidden": false, "isDuplicate": true, "masterStringId": "1", "revision": "1", "hasPlurals": false, "labelIds": [ 3, 8 ], "url": "https://example.crowdin.com/translate/file-format-samples/1/en-uk/78#1", "createdAt": "2022-04-20T12:43:57+00:00", "updatedAt": "2022-04-20T13:24:01+00:00", "file": { "id": "44", "name": "umbrella_app.xliff", "title": "source_app_info", "type": "xliff", "path": "/directory1/directory2/filename.extension", "status": "active", "revision": "1", "branchId": "34", "directoryId": "4" }, "project": { "id": "777", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://crowdin.com/project/umbrella", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" }, "commentResolver": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" }, "mention": { "userIds": [ 1 ] } } } ``` * String-based project ```json { "event": "stringComment.deleted", "comment": { "id": "12", "text": "@BeMyEyes Please provide more details on where the text will be used", "type": "issue", "issueType": "source_mistake", "issueStatus": "unresolved", "resolvedAt": "2019-09-20T11:05:24+00:00", "createdAt": "2019-09-20T11:05:24+00:00", "string": { "id": "2814", "identifier": "b068931cc450442b63f5b3d276ea4297", "key": "name", "text": "Not all videos are shown to users. See more", "type": "text", "context": "shown on main page", "maxLength": "35", "isHidden": false, "isDuplicate": true, "masterStringId": "1", "hasPlurals": false, "labelIds": [ 3, 8 ], "url": "https://example.crowdin.com/translate/umbrella-sb/1/en-uk/78#1", "createdAt": "2022-04-20T12:43:57+00:00", "updatedAt": "2022-04-20T13:24:01+00:00", "branch": { "id": "34" }, "project": { "id": "627210", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella-sb", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://example.crowdin.com/u/projects/627210", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" }, "commentResolver": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" }, "mention": { "userIds": [ 1 ] } } } ``` ### [String Comment/Issue Restored](#string-commentissue-restored) [Section titled “String Comment/Issue Restored”](#string-commentissue-restored) * File-based project ```json { "event": "stringComment.restored", "comment": { "id": "12", "text": "@BeMyEyes Please provide more details on where the text will be used", "type": "issue", "issueType": "source_mistake", "issueStatus": "unresolved", "resolvedAt": "2019-09-20T11:05:24+00:00", "createdAt": "2019-09-20T11:05:24+00:00", "string": { "id": "2814", "identifier": "b068931cc450442b63f5b3d276ea4297", "key": "name", "text": "Not all videos are shown to users. See more", "type": "text", "context": "shown on main page", "maxLength": "35", "isHidden": false, "isDuplicate": true, "masterStringId": "1", "revision": "1", "hasPlurals": false, "labelIds": [ 3, 8 ], "url": "https://example.crowdin.com/translate/file-format-samples/1/en-uk/78#1", "createdAt": "2022-04-20T12:43:57+00:00", "updatedAt": "2022-04-20T13:24:01+00:00", "file": { "id": "44", "name": "umbrella_app.xliff", "title": "source_app_info", "type": "xliff", "path": "/directory1/directory2/filename.extension", "status": "active", "revision": "1", "branchId": "34", "directoryId": "4" }, "project": { "id": "777", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://crowdin.com/project/umbrella", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" }, "commentResolver": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" }, "mention": { "userIds": [ 1 ] } } } ``` * String-based project ```json { "event": "stringComment.restored", "comment": { "id": "12", "text": "@BeMyEyes Please provide more details on where the text will be used", "type": "issue", "issueType": "source_mistake", "issueStatus": "unresolved", "resolvedAt": "2019-09-20T11:05:24+00:00", "createdAt": "2019-09-20T11:05:24+00:00", "string": { "id": "2814", "identifier": "b068931cc450442b63f5b3d276ea4297", "key": "name", "text": "Not all videos are shown to users. See more", "type": "text", "context": "shown on main page", "maxLength": "35", "isHidden": false, "isDuplicate": true, "masterStringId": "1", "hasPlurals": false, "labelIds": [ 3, 8 ], "url": "https://example.crowdin.com/translate/umbrella-sb/1/en-uk/78#1", "createdAt": "2022-04-20T12:43:57+00:00", "updatedAt": "2022-04-20T13:24:01+00:00", "branch": { "id": "34" }, "project": { "id": "627210", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella-sb", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://example.crowdin.com/u/projects/627210", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true } }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" }, "commentResolver": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" }, "mention": { "userIds": [ 1 ] } } } ``` ### [Task Added](#task-added) [Section titled “Task Added”](#task-added) * File-based project ```json { "event": "task.added", "task": { "id": "1", "type": "1", "vendor": "gengo", "status": "todo", "title": "French", "assignees": [ { "id": 1, "username": "john_smith", "fullName": "John Smith", "avatarUrl": "", "wordsCount": 5, "wordsLeft": 3 } ], "assignedTeams": [ { "id": 1, "wordsCount": 5 } ], "fileIds": [ 1 ], "progress": { "total": 24, "done": 15, "percent": 2 }, "description": "Proofread all French strings", "hash": "dac37aff364d83899128e68afe0de4994", "translationUrl": "https://example.crowdin.com/proofread/9092638ac9f2a2d1b5571d08edc53763/all/en-fr/10?task=dac37aff364d83899128e68afe0de4994", "wordsCount": "24", "filesCount": "2", "commentsCount": "0", "deadline": "2022-08-23T18:00:00+00:00", "timeRange": "2022-04-09 00:00:00|2022-05-08 23:59:59", "workflowStepId": "10", "buyUrl": "https://www.paypal.com/cgi-bin/webscr?cmd=...", "createdAt": "2022-05-23T16:14:18+00:00", "updatedAt": "2022-05-23T18:02:19+00:00", "sourceLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "project": { "id": "777", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://crowdin.com/project/umbrella", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true }, "taskCreator": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" } } } ``` * String-based project ```json { "event": "task.added", "task": { "id": "1", "type": "1", "vendor": "gengo", "status": "todo", "title": "French", "assignees": [ { "id": 1, "username": "john_smith", "fullName": "John Smith", "avatarUrl": "", "wordsCount": 5, "wordsLeft": 3 } ], "assignedTeams": [ { "id": 1, "wordsCount": 5 } ], "branchIds": "", "progress": { "total": 24, "done": 15, "percent": 2 }, "description": "Proofread all French strings", "translationUrl": "https://example.crowdin.com/proofread/9092638ac9f2a2d1b5571d08edc53763/all/en-fr/10?task=dac37aff364d83899128e68afe0de4994", "wordsCount": "24", "commentsCount": "0", "deadline": "2022-08-23T18:00:00+00:00", "timeRange": "2022-04-09 00:00:00|2022-05-08 23:59:59", "workflowStepId": "10", "buyUrl": "https://www.paypal.com/cgi-bin/webscr?cmd=...", "createdAt": "2022-05-23T16:14:18+00:00", "updatedAt": "2022-05-23T18:02:19+00:00", "sourceLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "project": { "id": "627210", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella-sb", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://example.crowdin.com/u/projects/627210", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true }, "taskCreator": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" } } } ``` ### [Task Status Changed](#task-status-changed) [Section titled “Task Status Changed”](#task-status-changed) * File-based project ```json { "event": "task.statusChanged", "task": { "id": "1", "type": "1", "vendor": "gengo", "status": "todo", "title": "French", "assignees": [ { "id": 1, "username": "john_smith", "fullName": "John Smith", "avatarUrl": "", "wordsCount": 5, "wordsLeft": 3 } ], "assignedTeams": [ { "id": 1, "wordsCount": 5 } ], "fileIds": [ 1 ], "progress": { "total": 24, "done": 15, "percent": 2 }, "description": "Proofread all French strings", "hash": "dac37aff364d83899128e68afe0de4994", "translationUrl": "https://example.crowdin.com/proofread/9092638ac9f2a2d1b5571d08edc53763/all/en-fr/10?task=dac37aff364d83899128e68afe0de4994", "wordsCount": "24", "filesCount": "2", "commentsCount": "0", "deadline": "2022-08-23T18:00:00+00:00", "timeRange": "2022-04-09 00:00:00|2022-05-08 23:59:59", "workflowStepId": "10", "buyUrl": "https://www.paypal.com/cgi-bin/webscr?cmd=...", "createdAt": "2022-05-23T16:14:18+00:00", "updatedAt": "2022-05-23T18:02:19+00:00", "oldStatus": "todo", "newStatus": "in_progress", "sourceLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "project": { "id": "777", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://crowdin.com/project/umbrella", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true }, "taskCreator": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" } } } ``` * String-based project ```json { "event": "task.statusChanged", "task": { "id": "1", "type": "1", "vendor": "gengo", "status": "todo", "title": "French", "assignees": [ { "id": 1, "username": "john_smith", "fullName": "John Smith", "avatarUrl": "", "wordsCount": 5, "wordsLeft": 3 } ], "assignedTeams": [ { "id": 1, "wordsCount": 5 } ], "branchIds": "", "progress": { "total": 24, "done": 15, "percent": 2 }, "description": "Proofread all French strings", "translationUrl": "https://example.crowdin.com/proofread/9092638ac9f2a2d1b5571d08edc53763/all/en-fr/10?task=dac37aff364d83899128e68afe0de4994", "wordsCount": "24", "commentsCount": "0", "deadline": "2022-08-23T18:00:00+00:00", "timeRange": "2022-04-09 00:00:00|2022-05-08 23:59:59", "workflowStepId": "10", "buyUrl": "https://www.paypal.com/cgi-bin/webscr?cmd=...", "createdAt": "2022-05-23T16:14:18+00:00", "updatedAt": "2022-05-23T18:02:19+00:00", "oldStatus": "todo", "newStatus": "in_progress", "sourceLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "project": { "id": "627210", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella-sb", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://example.crowdin.com/u/projects/627210", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true }, "taskCreator": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" } } } ``` ### [Task Updated](#task-updated) [Section titled “Task Updated”](#task-updated) * File-based project ```json { "event": "task.updated", "task": { "id": "1", "type": "1", "vendor": "gengo", "status": "todo", "title": "French", "assignees": [ { "id": 1, "username": "john_smith", "fullName": "John Smith", "avatarUrl": "", "wordsCount": 5, "wordsLeft": 3 } ], "assignedTeams": [ { "id": 1, "wordsCount": 5 } ], "fileIds": [ 1 ], "progress": { "total": 24, "done": 15, "percent": 2 }, "description": "Proofread all French strings", "translationUrl": "https://example.crowdin.com/proofread/9092638ac9f2a2d1b5571d08edc53763/all/en-fr/10?task=dac37aff364d83899128e68afe0de4994", "wordsCount": "24", "filesCount": "2", "commentsCount": "0", "deadline": "2022-08-23T18:00:00+00:00", "timeRange": "2022-04-09 00:00:00|2022-05-08 23:59:59", "workflowStepId": "10", "buyUrl": "https://www.paypal.com/cgi-bin/webscr?cmd=...", "createdAt": "2022-05-23T16:14:18+00:00", "updatedAt": "2022-05-23T18:02:19+00:00", "sourceLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "project": { "id": "777", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://crowdin.com/project/umbrella", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true }, "taskCreator": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" } } } ``` * String-based project ```json { "event": "task.updated", "task": { "id": "1", "type": "1", "vendor": "gengo", "status": "todo", "title": "French", "assignees": [ { "id": 1, "username": "john_smith", "fullName": "John Smith", "avatarUrl": "", "wordsCount": 5, "wordsLeft": 3 } ], "assignedTeams": [ { "id": 1, "wordsCount": 5 } ], "branchIds": "{{taskBranchIds}}", "progress": { "total": 24, "done": 15, "percent": 2 }, "description": "Proofread all French strings", "translationUrl": "https://example.crowdin.com/proofread/9092638ac9f2a2d1b5571d08edc53763/all/en-fr/10?task=dac37aff364d83899128e68afe0de4994", "wordsCount": "24", "commentsCount": "0", "deadline": "2022-08-23T18:00:00+00:00", "timeRange": "2022-04-09 00:00:00|2022-05-08 23:59:59", "workflowStepId": "10", "buyUrl": "https://www.paypal.com/cgi-bin/webscr?cmd=...", "createdAt": "2022-05-23T16:14:18+00:00", "updatedAt": "2022-05-23T18:02:19+00:00", "sourceLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "project": { "id": "627210", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella-sb", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://example.crowdin.com/u/projects/627210", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true }, "taskCreator": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" } } } ``` ### [Task Deleted](#task-deleted) [Section titled “Task Deleted”](#task-deleted) * File-based project ```json { "event": "task.deleted", "task": { "id": "1", "type": "1", "vendor": "gengo", "status": "todo", "title": "French", "assignees": [ { "id": 1, "username": "john_smith", "fullName": "John Smith", "avatarUrl": "", "wordsCount": 5, "wordsLeft": 3 } ], "assignedTeams": [ { "id": 1, "wordsCount": 5 } ], "fileIds": [ 1 ], "progress": { "total": 24, "done": 15, "percent": 2 }, "description": "Proofread all French strings", "hash": "dac37aff364d83899128e68afe0de4994", "translationUrl": "https://example.crowdin.com/proofread/9092638ac9f2a2d1b5571d08edc53763/all/en-fr/10?task=dac37aff364d83899128e68afe0de4994", "wordsCount": "24", "filesCount": "2", "commentsCount": "0", "deadline": "2022-08-23T18:00:00+00:00", "timeRange": "2022-04-09 00:00:00|2022-05-08 23:59:59", "workflowStepId": "10", "buyUrl": "https://www.paypal.com/cgi-bin/webscr?cmd=...", "createdAt": "2022-05-23T16:14:18+00:00", "updatedAt": "2022-05-23T18:02:19+00:00", "sourceLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "project": { "id": "777", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://crowdin.com/project/umbrella", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true }, "taskCreator": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" } } } ``` * String-based project ```json { "event": "task.deleted", "task": { "id": "1", "type": "1", "vendor": "gengo", "status": "todo", "title": "French", "assignees": [ { "id": 1, "username": "john_smith", "fullName": "John Smith", "avatarUrl": "", "wordsCount": 5, "wordsLeft": 3 } ], "assignedTeams": [ { "id": 1, "wordsCount": 5 } ], "branchIds": "", "progress": { "total": 24, "done": 15, "percent": 2 }, "description": "Proofread all French strings", "translationUrl": "https://example.crowdin.com/proofread/9092638ac9f2a2d1b5571d08edc53763/all/en-fr/10?task=dac37aff364d83899128e68afe0de4994", "wordsCount": "24", "commentsCount": "0", "deadline": "2022-08-23T18:00:00+00:00", "timeRange": "2022-04-09 00:00:00|2022-05-08 23:59:59", "workflowStepId": "10", "buyUrl": "https://www.paypal.com/cgi-bin/webscr?cmd=...", "createdAt": "2022-05-23T16:14:18+00:00", "updatedAt": "2022-05-23T18:02:19+00:00", "sourceLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "targetLanguage": { "id": "es", "name": "Spanish", "editorCode": "es", "twoLettersCode": "es", "threeLettersCode": "spa", "locale": "es-ES", "androidCode": "es-rES", "osxCode": "es.lproj", "osxLocale": "es", "textDirection": "ltr", "dialectOf": null }, "project": { "id": "627210", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella-sb", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://example.crowdin.com/u/projects/627210", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true }, "taskCreator": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" } } } ``` ### [Project Created](#project-created) [Section titled “Project Created”](#project-created) ```json { "event": "project.created", "project": { "id": "1", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://crowdin.com/project/umbrella", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true }, "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" } } ``` ### [Project Deleted](#project-deleted) [Section titled “Project Deleted”](#project-deleted) ```json { "event": "project.deleted", "project": { "id": "1", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://crowdin.com/project/umbrella", "cname": null, "languageAccessPolicy": "moderate", "visibility": "private", "publicDownloads": true }, "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" } } ``` ### [Group Created](#group-created) [Section titled “Group Created”](#group-created) ```json { "event": "group.created", "group": { "id": "1", "name": "Group Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "description": "Group Description", "parentId": "1", "userId": "1", "organizationId": "1" }, "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" } } ``` ### [Group Deleted](#group-deleted) [Section titled “Group Deleted”](#group-deleted) ```json { "event": "group.deleted", "group": { "id": "1", "name": "Group Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "description": "Group Description", "parentId": "1", "userId": "1", "organizationId": "1" }, "user": { "id": "1", "username": "john_smith", "fullName": "John Smith", "avatarUrl": "" } } ``` ## [Crowdin and Crowdin Enterprise Events Payloads](#crowdin-and-crowdin-enterprise-events-payloads) [Section titled “Crowdin and Crowdin Enterprise Events Payloads”](#crowdin-and-crowdin-enterprise-events-payloads) The payload structure for Crowdin and Crowdin Enterprise events is basically the same. The only difference is in the `project` object. For Crowdin Enterprise events, the `project` object contains additional fields. ```diff { "project": { "id": "777", "userId": "1", "sourceLanguageId": "en", "targetLanguageIds": [ "uk", "pl" ], "identifier": "umbrella", "name": "Project Name", "createdAt": "2022-04-20T11:05:24+00:00", "updatedAt": "2022-04-21T11:07:29+00:00", "lastActivity": "2022-04-21T11:07:29+00:00", "description": "Project Description", "url": "https://crowdin.com/project/umbrella", "cname": null, -"languageAccessPolicy": "moderate", -"visibility": "private", -"publicDownloads": true, +"logo": ""data:image/png;base64,iVBORw0KGgoAAAANSU....", "isExternal": false, "externalType": null, "hasCrowdsourcing": true, "groupId": 1 } } ```