Developer Portal: Developer Portal documentation # API > Learn how to use Crowdin's API to integrate localization into your process Crowdin’s API is a full-featured RESTful API that helps you integrate localization into your development process. The endpoints we use allow you to easily make calls to retrieve information and perform necessary actions. Most of the functionality of Crowdin is available through the API. It allows you to create projects for translations, add and update files, download translations, and much more. In this way, you can script the complex actions that your situation requires. [Crowdin API Reference ](/developer/api/v2/)File-based projects [Enterprise API Reference ](/developer/enterprise/api/v2/)File-based projects [Crowdin API Reference ](/developer/api/v2/string-based/)String-based projects [Enterprise API Reference ](/developer/enterprise/api/v2/string-based/)String-based projects [GraphQL API ](/developer/graphql-api/)GraphQL API is a tool that allows you to retrieve exactly the data you need using more specific and flexible queries. ## [API Clients](#api-clients) [Section titled “API Clients”](#api-clients) The Crowdin API clients are lightweight, open-source interfaces developed for the Crowdin API. They offer common services for making API requests. [Official ](https://github.com/crowdin/crowdin-api-client-js) [![Crowdin JavaScript client](/images/repo-card/crowdin-api-client-js.png)](https://github.com/crowdin/crowdin-api-client-js) ##### [Crowdin JavaScript client](https://github.com/crowdin/crowdin-api-client-js) [View and Install](https://github.com/crowdin/crowdin-api-client-js) [Official ](https://github.com/crowdin/crowdin-api-client-php) [![Crowdin PHP client](/images/repo-card/crowdin-api-client-php.png)](https://github.com/crowdin/crowdin-api-client-php) ##### [Crowdin PHP client](https://github.com/crowdin/crowdin-api-client-php) [View and Install](https://github.com/crowdin/crowdin-api-client-php) [Official ](https://github.com/crowdin/crowdin-api-client-java) [![Crowdin Java client](/images/repo-card/crowdin-api-client-java.png)](https://github.com/crowdin/crowdin-api-client-java) ##### [Crowdin Java client](https://github.com/crowdin/crowdin-api-client-java) [View and Install](https://github.com/crowdin/crowdin-api-client-java) [Official ](https://github.com/crowdin/crowdin-api-client-python) [![Crowdin Python client](/images/repo-card/crowdin-api-client-python.png)](https://github.com/crowdin/crowdin-api-client-python) ##### [Crowdin Python client](https://github.com/crowdin/crowdin-api-client-python) [View and Install](https://github.com/crowdin/crowdin-api-client-python) [Official ](https://github.com/crowdin/crowdin-api-client-ruby) [![Crowdin Ruby client](/images/repo-card/crowdin-api-client-ruby.png)](https://github.com/crowdin/crowdin-api-client-ruby) ##### [Crowdin Ruby client](https://github.com/crowdin/crowdin-api-client-ruby) [View and Install](https://github.com/crowdin/crowdin-api-client-ruby) [Official ](https://github.com/crowdin/crowdin-api-client-dotnet) [![Crowdin .NET client](/images/repo-card/crowdin-api-client-dotnet.png)](https://github.com/crowdin/crowdin-api-client-dotnet) ##### [Crowdin .NET client](https://github.com/crowdin/crowdin-api-client-dotnet) [View and Install](https://github.com/crowdin/crowdin-api-client-dotnet) [Official ](https://github.com/crowdin/crowdin-api-client-go) [![Crowdin Go client](/images/repo-card/crowdin-api-client-go.png)](https://github.com/crowdin/crowdin-api-client-go) ##### [Crowdin Go client](https://github.com/crowdin/crowdin-api-client-go) [View and Install](https://github.com/crowdin/crowdin-api-client-go) ## [See Also](#see-also) [Section titled “See Also”](#see-also) [Language Codes ](/developer/language-codes/)A list of language codes used in Crowdin and Crowdin Enterprise. # Mobile Apps Localization > Manage localization for iOS and Android apps without translating the same strings twice Effectively manage localization for iOS and Android apps without translating the same strings twice. ## [Localization Management within a Single Project](#localization-management-within-a-single-project) [Section titled “Localization Management within a Single Project”](#localization-management-within-a-single-project) Translate iOS and Android files within one Crowdin project. Use one of the options suggested below, depending on your project specifications. Recommendations: * Upload iOS (iOS Strings, iOS XLIFF) and Android (Android XML) app files into different locations (folders) in the Crowdin project. * Connect both Android and iOS repos to your Crowdin project to automate the synchronization of the source and translation files. * Use the **Duplicate Strings** option to manage duplicates. * Use the **Unify Placeholders** option to manage Android and iOS placeholders. ### [Hide Duplicates](#hide-duplicates) [Section titled “Hide Duplicates”](#hide-duplicates) Upload iOS and Android app files to one Crowdin project and select the **Hide** option in the project’s **Settings > Import > Source Strings**. Usually, the app developed for iOS and Android platforms share most of the source strings for iOS and Android files. So once you select the **Hide** option, the system will detect the duplicate strings for both types of files (iOS and Android) and hide them while keeping visible only the strings in the files uploaded first. Once the string that was uploaded first is translated, the hidden duplicate will get this translation automatically due to the selected **Hide** option. This way, translators will translate only the unique visible texts. When the translation of the visible string is updated, its hidden duplicates will also get the updated translation. Read more about [Duplicate Strings](/project-settings/import/#duplicate-strings). ### [Unify Placeholders](#unify-placeholders) [Section titled “Unify Placeholders”](#unify-placeholders) If some source strings for iOS and Android are the same but differ only in placeholders, we recommend selecting the **Unify Placeholders** option in the project’s **Settings > Import > Source Strings**. For example, you added the iOS string `Hello, %@!` and a similar one to Android `Hello, %s!`. The **Unify Placeholders** option will convert both to `Hello, [%s]!`, so translation from the Android file can migrate to iOS. On export, you will get translations with the original placeholders. It’s also possible to add/modify new strings online in the project for Android XML and iOS Strings files via the **Strings** section of the project. Read more about [String Editing](/string-management/#string-editing). ## [Localization Management within a Single Project with Bundles](#localization-management-within-a-single-project-with-bundles) [Section titled “Localization Management within a Single Project with Bundles”](#localization-management-within-a-single-project-with-bundles) Recommendations: * Upload either iOS (iOS Strings, iOS XLIFF) or Android (Android XML) app files to your Crowdin project. * Configure bundles to be able to export translations in the needed file format. Read more about [Bundles](/bundles/). ### [Translation Export using Bundles](#translation-export-using-bundles) [Section titled “Translation Export using Bundles”](#translation-export-using-bundles) Localize the resources of just one application within Crowdin, and download different file formats for both your Android and iOS apps. For example, you can upload an XML file to Crowdin for [Android localization](https://crowdin.com/blog/2022/08/10/android-app-localization-tutorial) and receive two files on export: XML for Android and Strings for iOS. You might need to make some slight adjustments to the exported files (translation keys will remain the same as in the Android file, so they might need adjustments for the iOS file). However, the localization time and expenses for translation services can be significantly reduced using this approach. ## [Next Steps in Mobile Apps Localization](#next-steps-in-mobile-apps-localization) [Section titled “Next Steps in Mobile Apps Localization”](#next-steps-in-mobile-apps-localization) Here are the next steps you might consider while localizing your mobile apps. As an alternative to a more traditional approach when dealing with source files, you can send strings for translation directly from your design tools with the help of Crowdin plugins. Another good option is to use Over-the-Air Content Delivery to update translated strings of your mobile apps instantly without a need to roll out a new version on the App Store or Google Play. Read more about [Android](https://crowdin.com/blog/2022/08/10/android-app-localization-tutorial) or iOS [mobile app localization](https://crowdin.com/blog/2021/02/11/smart-ways-to-approach-mobile-app-localization) on our blog. # Authorizing OAuth Apps > Learn how to enable organization members to authorize your OAuth app You can enable organization members to authorize your OAuth app. When you build an OAuth app, implement the web application flow described below to obtain an authorization code and then exchange it for a token. ## [Request Authorization Code](#request-authorization-code) [Section titled “Request Authorization Code”](#request-authorization-code) You should redirect the user to the `/oauth/authorize` endpoint with the following GET parameters: ```bash https://accounts.crowdin.com/oauth/authorize ``` This will ask the user to approve the app access to their account based on the scopes specified in REQUESTED\_SCOPES and then redirect back to the REDIRECT\_URI you provided when creating an app. ### [Parameters](#request-authorization-code-parameters) [Section titled “Parameters”](#request-authorization-code-parameters) | Name | Value | Description | | ---------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `client_id` | string | Required. You receive Client ID for the app when you register it. | | `redirect_uri` | string | Required. The URL in your application where users will be sent after authorization. | | `response_type:``code` | string | Required. The parameter is used for the flow specification of an OAuth app. | | `scope` | string | Required. Select the access your app requires from the list of scopes available. You can add multiple [scopes](/developer/understanding-scopes/) separated by spaces (no need to use quotation marks). | | `state` | string | An unguessable random string. Use it for extra protection against cross-site request forgery attacks. | The following Authorization Url will be created: ```bash https://accounts.crowdin.com/oauth/authorize?client_id=m50YenPpqac8u5D4dnK&redirect_uri=https://impact-mobile.com/auth/crowdin&response_type=code&scope=project+tm&state=d131dd02c5e6eec4 ``` After successful authorization users are redirected back to your site: ```bash https://impact-mobile.com/auth/crowdin/?code=def50200df1fbb5ebac05f9288850d9e...0835bd3cf42&state=d131dd02c5e6eec4 ``` If authorization has been declined, users are redirected to your website with an error: ```bash https://impact-mobile.com/auth/crowdin/?error=access_denied&state=d131dd02c5e6eec4 ``` ## [Users Are Redirected Back to Your Site by Crowdin](#users-are-redirected-back-to-your-site-by-crowdin) [Section titled “Users Are Redirected Back to Your Site by Crowdin”](#users-are-redirected-back-to-your-site-by-crowdin) If a user authorizes the app, Crowdin redirects back to your site and you can exchange the code received for an access token: ```bash POST https://accounts.crowdin.com/oauth/token ``` ### [Parameters](#redirect-parameters) [Section titled “Parameters”](#redirect-parameters) | Name | Value | Description | | ---------------------------------- | ------ | ----------------------------------------------------------------------------------- | | `grant_type:` `authorization_code` | string | Required. The parameter is used for the flow specification of an OAuth app. | | `client_id` | string | Required. You receive Client ID for the app when you register it. | | `client_secret` | string | Required. You receive Client Secret for the app when you register it. | | `redirect_uri` | string | Required. The URL in your application where users will be sent after authorization. | | `code` | string | Required. Code received from the callback query string. | For example, request in curl takes the following form: ```bash curl -X POST \ https://accounts.crowdin.com/oauth/token \ -H "content-type: application/json" \ -d "{ \"grant_type\":\"authorization_code\", \"client_id\":\"m50YenPpqac8u5D4dnK\", \"client_secret\":\"yz35kYtjox...YE9Am\", \"redirect_uri\":\"https://impact-mobile.com/auth/crowdin\", \"code\":\"def50200df1fbb5ebac05f9288850d9e...0835bd3cf42\" }" ``` ### [Response](#redirect-response) [Section titled “Response”](#redirect-response) By default, the response takes the following form: ```json { "access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJS...lag1e_Zk4EdJ5diYfz0", "token_type":"bearer", "expires_in": 7200, "refresh_token": "b213c684ccaa7db1217e946e6ad...fff7ae" } ``` ## [Make Requests to the API with the Access Token Returned](#make-requests-to-the-api-with-the-access-token-returned) [Section titled “Make Requests to the API with the Access Token Returned”](#make-requests-to-the-api-with-the-access-token-returned) The access token now allows you to make requests to Crowdin API on behalf of the authorized user. For example, in curl you can set the following Authorization header: ```bash curl -H "Authorization: Bearer ACCESS_TOKEN" https://api.crowdin.com/api/v2/projects ``` Crowdin Enterprise: ```bash curl -H "Authorization: Bearer ACCESS_TOKEN" https://.api.crowdin.com/api/v2/projects ``` Read more about [JWT Token Structure](/developer/crowdin-apps-security/). ## [Refresh Token](#refresh-token) [Section titled “Refresh Token”](#refresh-token) The access token received after a user authorizes the app has an expiration time. Access token expires in the number of seconds defined in the response. To refresh a token without requiring the user to be redirected, send a POST request with the following body parameters to the authorization server: ```bash POST https://accounts.crowdin.com/oauth/token ``` ### [Parameters](#refresh-token-parameters) [Section titled “Parameters”](#refresh-token-parameters) | Name | Value | Description | | ----------------------------- | ------ | --------------------------------------------------------------------------- | | `grant_type:` `refresh_token` | string | Required. The parameter is used for the flow specification of an OAuth app. | | `client_id` | string | Required. You receive Client ID for the app when you register it. | | `client_secret` | string | Required. You receive Client Secret for the app when you register it. | | `refresh_token` | string | Required. Refresh token received from the last authorization response. | For example, request in curl takes the following form: ```bash curl -X POST \ https://accounts.crowdin.com/oauth/token \ -H "content-type: application/json" \ -d "{ \"grant_type\":\"refresh_token\", \"client_id\":\"m50YenPpqac8u5D4dnK\", \"client_secret\":\"yz35kYtjox...YE9Am\", \"refresh_token\":\"b213c684ccaa7db1217e946e6ad...fff7ae\" }" ``` ### [Response](#refresh-token-response) [Section titled “Response”](#refresh-token-response) By default, the response takes the following form: ```json { "access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJS...ZjFkMWI4OWFlIiwiaWF", "token_type":"bearer", "expires_in": 7200, "refresh_token": "ea506ea4c37aa152f0a91ed2482...4a0c567" } ``` ## [Redirect URLs](#redirect-urls) [Section titled “Redirect URLs”](#redirect-urls) You can register one or more redirect URLs when you create an OAuth Application on Crowdin. For security reasons, if the URL is not included in the Application info you won’t be able to redirect users to this URL after authorization. # Automating Screenshot Management > Learn how to automate screenshot creation and updates with Crowdin In-Context and Playwright This guide explains how to automate generating and updating screenshots in Crowdin [In-Context](/developer/in-context-localization/). By automating screenshot creation, translators gain accurate and up-to-date visual context, improving the localization process and translation quality. The guide covers prerequisites, setup instructions, capturing screenshots across web pages, and uploading them to Crowdin via API. ## [Prerequisites](#prerequisites) [Section titled “Prerequisites”](#prerequisites) Before proceeding, ensure that your website integrates [Crowdin In-Context functionality](/developer/in-context-localization/#integration). ### [Account Setup](#account-setup) [Section titled “Account Setup”](#account-setup) To configure your account for automation to function properly, follow these steps: 1. **Enable 2FA in Crowdin**: Open your project and go to **Settings > Privacy & collaboration > Privacy** to set up Two-factor authentication for login. 2. **Disable Device Verification**: Device verification ensures account security but can interrupt automated workflows. Disabling it for testing environments ensures uninterrupted automation. * For Crowdin: Go to **Account Settings > Account > New device verification** and disable the setting. * For Crowdin Enterprise: Go to **Account Settings > Security > Device Verification** and disable the setting. 3. **Generate a Secret Key**: Obtain the secret key for generating 2FA tokens. This key is required for programmatically creating one-time passwords (OTPs) using the `otplib` library. ### [Dependencies](#dependencies) [Section titled “Dependencies”](#dependencies) This guide uses the following dependencies for browser automation and OTP generation: * [Playwright](https://playwright.dev/): A modern testing framework for browser automation, ideal for navigating and interacting with web pages. * [`otplib`](https://www.npmjs.com/package/otplib): A library for generating one-time passwords (OTPs) programmatically, essential for bypassing 2FA in automated workflows. Run the following command to install the dependencies: * npm ```bash npm install -D @playwright/test otplib ``` * Yarn ```bash yarn add -D @playwright/test otplib ``` * pnpm ```bash pnpm add -D @playwright/test otplib ``` ## [Capturing Screenshots with Crowdin In-Context](#capturing-screenshots-with-crowdin-in-context) [Section titled “Capturing Screenshots with Crowdin In-Context”](#capturing-screenshots-with-crowdin-in-context) Crowdin provides the `window.jipt.capture_screenshot(name: string, options: object | null)` method to automate screenshot generation. In addition to screenshots, this method collects metadata to provide translators with detailed and accurate context for their work. The following sections explain how to implement this functionality using Playwright. ### [Code Example](#code-example) [Section titled “Code Example”](#code-example) The following script illustrates navigating a website, logging in, capturing screenshots, and uploading them to Crowdin for translators’ reference: ```js // @ts-check const { test, expect } = require('@playwright/test'); const { authenticator } = require('otplib'); test('Capture Crowdin Screenshots', async ({ page }) => { const siteUrl = 'https://example.com'; // Navigate to the application await page.goto(siteUrl); // Log in await page.locator('#jipt-login-panel iframe').contentFrame().getByRole('button', { name: 'Log In' }).click(); await page.getByLabel('Email or Username').fill('username'); await page.getByLabel('Password').fill('password'); await page.getByRole('button', { name: 'Log In' }).click(); // Handle Two-Factor Authentication (if applicable) const token = authenticator.generate('KEY'); // Replace 'KEY' with your 2FA secret await page.getByLabel('Verification Code').fill(token); await page.getByRole('button', { name: 'Log In' }).click(); // Confirm login and start capturing screenshots await page.getByRole('button', { name: 'Keep Me Logged In' }).click(); // Capture the first screenshot await page.goto(siteUrl); await page.locator('#crowdin-jipt-mask').click(); await expect(page.locator('h1')).toContainText('Crowdin HTML Sample'); await page.evaluate(() => { return new Promise((resolve, reject) => { window.jipt.capture_screenshot('HTML Sample File', { success: resolve, error: reject }); }); }); // Capture a second screenshot on another page await page.goto(`${siteUrl}/second`); await expect(page.locator('h1')).toContainText('Second Crowdin HTML Sample'); await page.evaluate(() => { return new Promise((resolve, reject) => { window.jipt.capture_screenshot('Second HTML Sample File', { override: false, success: resolve, error: reject }); }); }); }); ``` ### [Key Code Implementation Details](#key-code-implementation-details) [Section titled “Key Code Implementation Details”](#key-code-implementation-details) * **Navigating Pages**: Use `page.goto(url)` to navigate to specific pages in your application. * **Logging In**: Simulate user actions, such as filling out forms and clicking buttons, using Playwright’s locators like `getByRole()` and `getByLabel()`. * **Capturing Screenshots**: Use `window.jipt.capture_screenshot()` to generate and upload screenshots to Crowdin. The method accepts the following parameters: * `name: string`: The name of the screenshot. * `options: object | null`: Custom settings: * `override: boolean`: The `override` parameter determines how Crowdin handles screenshots with duplicate names. Use `true` (default) to overwrite the first matching screenshot or `false` to create a new one, even if the name matches. * `success: function`: A callback function triggered on successful upload. It receives an object with `{msg_type: 'make_screenshot_data', screenshot_name: string}`, which provides the type of the message and the name of the uploaded screenshot. * `error: function`: A callback function triggered on upload failure. It receives an object with `{msg_type: 'make_screenshot_error', response: object}` or an `Error`, containing details about the failure. * **Two-Factor Authentication**: Use the `otplib` library to programmatically generate OTP tokens when 2FA is enabled. Replace `'KEY'` with your project’s secret key for valid OTP generation. # Configuration File > Explore the possibilities of the crowdin.yml configuration file The various Crowdin tools use a `crowdin.yml` configuration file that specifies the resources to be managed, including the files to be uploaded to Crowdin and the locations of the corresponding translations. [VCS Integrations ](/integrations/#vcs-integrations)GitHub, GitLab, Bitbucket, Azure Repos. [Console Client (CLI) ](https://crowdin.github.io/crowdin-cli/)Cross-platform command-line tool. [Visual Studio Code Plugin ](https://marketplace.visualstudio.com/items?itemName=Crowdin.vscode-crowdin) [Android Studio Plugin ](https://github.com/crowdin/android-studio-plugin#readme) ## [Configuration File Structure](#configuration-file-structure) [Section titled “Configuration File Structure”](#configuration-file-structure) A valid `crowdin.yml` configuration file has the following structure, so please ensure that you fill out all the needed information: * Head of the file with project credentials, preferences, and access information. * One files array element that describes a set of source and translation files you will manage. * Required fields in the files array: `source` that defines filters for the source files and `translation` with instructions on where to store the translated files or where to look for translations you already have if you want to upload them while setting up the CLI. ## [Writing a Simple Configuration File](#writing-a-simple-configuration-file) [Section titled “Writing a Simple Configuration File”](#writing-a-simple-configuration-file) A typical configuration file looks similar to the following: crowdin.yml ```yml "project_id": "projectId" "api_token": "personal-access-token" "base_path": "." "base_url": "https://api.crowdin.com" "preserve_hierarchy": true "files": [ { "source": "/locale/en/folder1/[0-2].txt", "translation": "/locale/%two_letters_code%/folder1/%original_file_name%" }, ] ``` | Name | Description | | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `project_id` | Crowdin Project numeric ID | | `api_token` | Crowdin Personal Access Token. The token owner should have at least Manager permissions in the project | | `base_path`optional | Path to your project directory on a local machine relative to the `crowdin.yml` configuration file | | `base_url` optional | Crowdin API base URL. Optional for crowdin.com. For Crowdin Enterprise use the `https://{organization-name}.api.crowdin.com` | | `preserve_hierarchy`optional | If set to `true`, the directory structure will be preserved on the server. If set to `false`, the directory structure will be flattened. | | `source` | Filter for the source files. Wildcards are supported. | | `translation` | Path to store the translated files. Wildcards are supported. | ## [API Credentials from Environment Variables](#api-credentials-from-environment-variables) [Section titled “API Credentials from Environment Variables”](#api-credentials-from-environment-variables) You could load the API Credentials from an environment variable, for example: crowdin.yml ```yml "project_id_env": "CROWDIN_PROJECT_ID" "api_token_env": "CROWDIN_PERSONAL_TOKEN" "base_path_env": "CROWDIN_BASE_PATH" "base_url_env": "CROWDIN_BASE_URL" ``` If mixed, *api\_token* and *project\_id* are prioritized: crowdin.yml ```yml "project_id_env": "CROWDIN_PROJECT_ID" # Low priority "api_token_env": "CROWDIN_PERSONAL_TOKEN" # Low priority "base_path_env": "CROWDIN_BASE_PATH" # Low priority "base_url_env": "CROWDIN_BASE_PATH" # Low priority "project_id": "projectId" # High priority "api_token": "personal-access-token" # High priority "base_path": "." # High priority "base_url": "https://api.crowdin.com" # High priority ``` ## [General Configuration](#general-configuration) [Section titled “General Configuration”](#general-configuration) The sample configuration provided above has source and translation attributes containing standard wildcards (also known as globbing patterns) to make it easier to work with multiple files. Here are patterns you can use: *** **\*** (asterisk) Matches any character in the file or directory name. If you specify `*.json`, it will include all files like `messages.json`, `about_us.json`, and anything that ends with `.json`. *** **\*\*** (doubled asterisk) Matches any string recursively (including subdirectories). Note that you can use `**` in both the source and translation patterns. When you use `**` in the translation pattern, it always includes a sub-path from the source for a given file. For example, you can use source: `/en/**/*.po` to recursively upload all `*.po` files to Crowdin Enterprise. The translation pattern will be `/%two_letters_code%/**/%original_file_name%`. *** **?** (question mark) Matches any single character. *** **\[set]** Matchesany single character in a set. Behaves exactly like character sets in Regexp, including set negation (`[^a-z]`). *** **\\** (backslash) Escapes the next metacharacter. ## [Placeholders](#placeholders) [Section titled “Placeholders”](#placeholders) Crowdin CLI allows to use the following placeholders to put appropriate variables into the resulting file name: | **Name** | **Description** | | -------------------------- | ----------------------------------------------------------------------------------------------------- | | `%original_file_name%` | Original file name | | `%original_path%` | Take parent folder names in the Crowdin Enterprise project to build file path in the resulting bundle | | `%file_extension%` | Original file extension | | `%file_name%` | File name without extension | | `%language%` | Language name (e.g., Ukrainian) | | `%two_letters_code%` | Language code ISO 639-1 (e.g., uk) | | `%three_letters_code%` | Language code ISO 639-2/T (e.g., ukr) | | `%locale%` | Locale (e.g., uk-UA) | | `%locale_with_underscore%` | Locale (e.g., uk\_UA) | | `%android_code%` | Android Locale identifier used to name “values-” directories | | `%osx_code%` | OS X Locale identifier used to name “.lproj” directories | | `%osx_locale%` | OS X locale used to name translation resources (e.g., uk, zh-Hans, zh\_HK) | You can also define the path for files in the resulting archive by putting a slash (`/`) at the beginning of the pattern. ## [Usage of Wildcards](#usage-of-wildcards) [Section titled “Usage of Wildcards”](#usage-of-wildcards) Structure of files and directories on the local machine: Example 1. Usage of wildcards in the source path: crowdin.yml ```yml "files": [ { "source": "/**/?[0-9].txt", # upload a1.txt, folder/a1.txt "translation": "/**/%two_letters_code%_%original_file_name%" }, { "source": "/**/*\\?*.txt", # upload crowdin?test.txt, folder/crowdin?test.txt "translation": "/**/%two_letters_code%_%original_file_name%" }, { "source": "/**/[^0-2].txt", # upload 3.txt, folder/3.txt, a.txt, folder/a.txt (ignore 1.txt, folder/1.txt) "translation": "/**/%two_letters_code%_%original_file_name%" } ] ``` Example 2. Usage of wildcards for ignoring files: crowdin.yml ```yml "files": [ { "source": "/**/*.*", #upload all files that the base_path contains "translation": "/**/%two_letters_code%_%original_file_name%", "ignore": [ "/**/%two_letters_code%_%original_file_name%", #ignore the translated files "/**/?.txt", #ignore 1.txt, a.txt, folder/1.txt, folder/a.txt "/**/[0-9].txt", #ignore 1.txt, folder/1.txt "/**/*\\?*.txt", #ignore crowdin?test.txt, folder/crowdin?test.txt "/**/[0-9][0-9][0-9].txt", #ignore 123.txt , folder/123.txt "/**/[0-9]*_*.txt" #ignore 123_test.txt, folder/123_test.txt ] } ] ``` ## [Renaming Translations File](#renaming-translations-file) [Section titled “Renaming Translations File”](#renaming-translations-file) If you need to rename a file with translations after the export, you can easily do this with the help of the parameter `translation_replace`. For example, if the file is named `strings_en.po`, it can be renamed to `strings.po`. For this, add a new parameter (at the same level as the translation parameter) to the configuration file: crowdin.yml ```yml "files": [ { "source": "/locale/**/*_en.po", "translation": "/locale/**/%original_file_name%_%two_letters_code%", "translation_replace": { "_en": "" } } ] ``` In this case, `_en` will be erased from the file name. ## [Ignoring Files and Directories](#ignoring-files-and-directories) [Section titled “Ignoring Files and Directories”](#ignoring-files-and-directories) From time to time, there are files and directories you don’t need to translate in Crowdin. In such cases, local per-file rules can be added to the config file in your project. crowdin.yml ```yml "files": [ { "source": "/**/*.properties", "translation": "/**/%file_name%_%two_letters_code%.%file_extension%", "ignore": [ "/test/file.properties", "/example.properties" ] }, { "source": "/locale/en/**/*.po", "translation": "/locale/%two_letters_code%/**/%original_file_name%", "ignore": [ "/locale/en/templates", "/locale/en/workflow" ] } ] ``` You can also use [wildcards](#usage-of-wildcards) to ignore files. ## [Excluding Target Languages for Source Files](#excluding-target-languages-for-source-files) [Section titled “Excluding Target Languages for Source Files”](#excluding-target-languages-for-source-files) By default, the source files are available for translation into all target languages of the project. If some source files shouldn’t be translated into specific target languages, you can exclude them with the help of the parameter `excluded_target_languages`. Configuration file example: crowdin.yml ```yml "files": [ { "source": "/resources/en/*.json", "translation": "/resources/%two_letters_code%/%original_file_name%", "excluded_target_languages": [ "uk", "fr" ] } ] ``` ## [Multilingual Spreadsheet](#multilingual-spreadsheet) [Section titled “Multilingual Spreadsheet”](#multilingual-spreadsheet) If a CSV or XLS/XLSX file contains the translations for all target languages, you should specify appropriate language codes in the scheme. CSV file example: ```csv identifier,source_phrase,context,Ukrainian,Russian,French ident1,Source 1,Context 1,,, ident2,Source 2,Context 2,,, ident3,Source 3,Context 3,,, ``` Configuration file example: crowdin.yml ```yml "files": [ { "source": "multicolumn.csv", "translation": "multicolumn.csv", "first_line_contains_header": true, "scheme": "identifier,source_phrase,context,uk,ru,fr" } ] ``` If your CSV or XLS/XLSX file contains columns that should be skipped on import, use `none` for such columns in the scheme, for example: crowdin.yml ```yml "scheme" : "identifier,source_phrase,context,uk,none,ru,none,fr" ``` ##### [Scheme Constants](#scheme-constants) [Section titled “Scheme Constants”](#scheme-constants) To form the scheme for your CSV or XLS/XLSX file, use the following constants: * `identifier` – Column contains string identifiers. * `source_phrase` – Column contains source strings. * `source_or_translation` – Column contains source strings, but the same column will be filled with translations when the file is exported. When uploading existing translations, the values from this column will be used as translations. * `translation` – Column contains translations. * `context` – Column contains comments or context information for the source strings. * `max_length` – Column contains max.length limit values for the translations of the strings. * `labels` – Column contains labels for the source strings. * `none` – Column that will be skipped on import. ## [Saving Directory Structure on Server](#saving-directory-structure-on-server) [Section titled “Saving Directory Structure on Server”](#saving-directory-structure-on-server) You can use the `preserve_hierarchy` option to preserve or flatten the directory structure of your source files in the Crowdin project. Example of the configuration file using the `preserve_hierarchy` option: crowdin.yml ```yml "preserve_hierarchy": true "files": [ { "source": "/locale/en/**/*.po", "translation": "/locale/%two_letters_code%/**/%original_file_name%" } ] ``` Let’s say the file/folder structure on your machine looks like this: If you don’t use the `"preserve_hierarchy": true` option in your configuration file at all or use it with the `false` value, all shared directories will be skipped, and the file structure in Crowdin will be represented as follows: Using the `"preserve_hierarchy": true` option, the file structure in Crowdin will be represented as follows: The directories that don’t contain any files for translation won’t be created in Crowdin (i.e., as the `emails` directory in the example above). ## [Uploading Files to Specified Path with Specified Type](#uploading-files-to-specified-path-with-specified-type) [Section titled “Uploading Files to Specified Path with Specified Type”](#uploading-files-to-specified-path-with-specified-type) This feature adds support for two optional parameters in the yml file section: `dest` and `type`. It’s typically used for projects where the uploaded name must be different so that Crowdin can detect the type correctly. The `dest` parameter allows you to specify a file name in Crowdin. It works for multiple files at once and supports the following placeholders: `%original_file_name%`, `%original_path%`, `%file_extension%`, `%file_name%`. Caution If you use the `dest` parameter, the configuration file should include the `preserve_hierarchy` parameter with the `true` value. Example of the configuration file with both parameters: crowdin.yml ```yml "files": [ { "source": "/conf/**/*.txt", "dest": "/conf/**/%file_name%.properties", "translation": "/conf/**/%two_letters_code%/%file_name%.properties", "type": "properties" }, { "source": "/app/*.txt", "dest": "/app/%file_name%.xml", "translation": "/res/values-%android_code%/%original_file_name%", "type": "android" } ] ``` ## [Changed Strings Update](#changed-strings-update) [Section titled “Changed Strings Update”](#changed-strings-update) You can use the `update_option` parameter to preserve translations for changed strings during the file update. If it is not set, translations for changed strings will be lost. Useful for typo fixes and minor changes in source strings. Depending on the value, `update_option` is used to preserve translations and preserve/remove validations of changed strings during file update. Acceptable values: * `update_as_unapproved` - preserve translations of changed strings and remove validations of those translations if they exist * `update_without_changes` - preserve translations and validations of changed strings Example of the configuration with the `update_option` parameter: crowdin.yml ```yml "files": [ { "source": "/*.csv", "translation": "/%three_letters_code%/%file_name%.csv", "first_line_contains_header": true, "scheme": "identifier,source_phrase,translation,context", "update_option": "update_as_unapproved" }, { "source": "/**/*.xlsx", "translation": "/%three_letters_code%/folder/%file_name%.xlsx", "update_option": "update_without_changes" } ] ``` ## [Custom Segmentation](#custom-segmentation) [Section titled “Custom Segmentation”](#custom-segmentation) Upload your XML, HTML, MD, or any other source files without a key-value structure with your own segmentation rules. If not specified, the pre-defined segmentation rules (SRX 2.0) are used for automatic content segmentation. Example of the configuration file custom segmentation: crowdin.yml ```yml "files": [ { "source": "/emails/sample1.html", "translation": "/emails/%locale%/%original_file_name%", "custom_segmentation": "/rules/sample.srx.xml" } ] ``` ## [Import Options](#import-options) [Section titled “Import Options”](#import-options) You can use additional parameters to customize the import process for specific file types. ### [XML Files Import Options](#xml-files-import-options) [Section titled “XML Files Import Options”](#xml-files-import-options) | Option | Type | Description | | -------------------------------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `translate_content` optional | bool | Defines whether to translate texts placed inside the tags. Acceptable values are 0 or 1. Default is 1. | | `translate_attributes`optional | bool | Defines whether to translate tags’ attributes. Acceptable values are 0 or 1. Default is 1. | | `content_segmentation`optional | bool | Defines whether to split long texts into smaller text segments. Acceptable values are 0 or 1. Default is 1. **Note!** When Content segmentation is enabled, the translation upload is handled by an experimental machine learning technology. | | `translatable_elements` optional | array | This is an array of strings, where each item is the XPaths to DOM element that should be imported. Sample path: `/path/to/node` or `/path/to/attribute[@attr]` **Note!** If defined, the parameters `translate_content` and `translate_attributes` are not taken into account while importing. | Example of the configuration with additional parameters: crowdin.yml ```yml "files": [ { "source": "/app/sample1.xml", "translation": "/app/%locale%/%original_file_name%", "translate_attributes": 1, "translate_content": 0 }, { "source": "/app/sample2.xml", "translation": "/app/%locale%/%original_file_name%", "translatable_elements": [ "/content/text", # translatable texts are stored in 'text' nodes of parent node 'content' "/content/text[@value]" # translatable texts are stored in 'value' attribute of 'text' nodes ] } ] ``` ### [Other Files Import Options](#other-files-import-options) [Section titled “Other Files Import Options”](#other-files-import-options) | Option | Type | Description | | ------------------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `content_segmentation` optional | bool | Defines whether to split long texts into smaller text segments. Only for TXT, DOCX, DITA, IDML, MEDIAWIKI, HTML, Front Matter HTML, Markdown, Front Matter Markdown, XML, XLIFF, XLIFF 2.0 Acceptable values are 0 or 1. Default is 1. **Note:** When Content segmentation is enabled, the translation upload is handled by an experimental machine learning technology. | Example of the configuration with additional parameters: crowdin.yml ```yml "files": [ { "source": "/emails/sample1.html", "translation": "/emails/%locale%/%original_file_name%", "content_segmentation": 1 } ] ``` ## [Export Options](#export-options) [Section titled “Export Options”](#export-options) You can use additional parameters to customize the export process for specific file types. ### [Java .properties File Format](#java-properties-file-format) [Section titled “Java .properties File Format”](#java-properties-file-format) ##### [Escape Quotes](#escape-quotes) [Section titled “Escape Quotes”](#escape-quotes) Defines whether a single quote should be escaped by another single quote or backslash in exported translations. You can add the `escape_quotes` per-file option. Acceptable values: * `0` - do not escape single quote * `1` - escape single quote with another single quote * `2` - escape single quote with a backslash * `3` - escape single quote with another single quote only in strings containing variables (default) ##### [Escape Special Characters](#escape-special-characters) [Section titled “Escape Special Characters”](#escape-special-characters) Defines whether any special characters (`=`, `:`, `!` and `#`) should be escaped by a backslash in exported translations. You can add the `escape_special_characters` per-file option. Acceptable values: * `0` - do not escape special characters * `1` - escape special characters by a backslash (default) Example of the configuration: crowdin.yml ```yml "files": [ { "source": "/en/strings.properties", "translation": "/%two_letters_code%/%original_file_name%", "escape_quotes": 1, "escape_special_characters": 0 } ] ``` ## [Configuration File for VCS Integrations](#configuration-file-for-vcs-integrations) [Section titled “Configuration File for VCS Integrations”](#configuration-file-for-vcs-integrations) VCS integrations require the same configuration file as the CLI tool, meaning the same structure is supported. The only difference is that you should not store the project credentials in the file header for security reasons. Also, you can use a few additional parameters. ### [Pull Request Title and Labels](#pull-request-title-and-labels) [Section titled “Pull Request Title and Labels”](#pull-request-title-and-labels) The default pull request title is `New Crowdin updates`. To specify your custom pull request title and add labels to the pull request, you can use the following parameters in the configuration file: `pull_request_title`, `pull_request_labels`. crowdin.yml ```yml "pull_request_title": "Custom PR title" "pull_request_labels": [ "crowdin", "l10n" ] ``` Limitations Pull request labels are not supported by the Bitbucket integration. ### [Commit Message](#commit-message) [Section titled “Commit Message”](#commit-message) Each time translations are committed the default message is shown `New translations {fileName} ({languageName})`. You can use the `commit_message` parameter to add Git tags (e.g., to skip builds). crowdin.yml ```yml "commit_message": "[ci skip]" ``` To replace the default commit message, use the `append_commit_message` parameter with the `false` value. You can also add two optional placeholders: `%original_file_name%` and `%language%` to use the appropriate file name and language variables accordingly. crowdin.yml ```yml "commit_message": "Fix: New translations %original_file_name% from Crowdin" "append_commit_message": false ``` ### [Export Languages](#export-languages) [Section titled “Export Languages”](#export-languages) By default, all the languages are exported. If you need to export some specific languages, use the `export_languages` parameter to specify them. crowdin.yml ```yml "export_languages": [ "uk", "ja" ] ``` ### [Pull Request Assignee](#pull-request-assignee) [Section titled “Pull Request Assignee”](#pull-request-assignee) If you need to assign a pull request to particular users, use the `pull_request_assignees` parameter to specify them. **GitHub/GitHub Server:** crowdin.yml ```yml "pull_request_assignees": [ "login1", "login2" ] ``` **GitLab/GitLab Self-Managed:** crowdin.yml ```yml "base_path": "." "pull_request_assignees": [ "user_id1", # numeric ID "user_id2" # numeric ID ] ``` Limitations Pull request assignee parameter is not supported by the Bitbucket, Bitbucket Server, and Azure Repos integrations. ### [Pull Request Reviewer](#pull-request-reviewer) [Section titled “Pull Request Reviewer”](#pull-request-reviewer) If you need to request a pull request review from particular users, use the `pull_request_reviewers` parameter to specify them. **GitHub/GitHub Server:** crowdin.yml ```yml "pull_request_reviewers": [ "login1", "login2" ] ``` **GitLab/GitLab Self-Managed:** crowdin.yml ```yml "pull_request_reviewers": [ "user_id1", # numeric ID "user_id2" # numeric ID ] ``` **Bitbucket:** crowdin.yml ```yml "pull_request_reviewers": [ "uuid1", # user uuid "uuid2" # user uuid ] ``` **Bitbucket Server:** crowdin.yml ```yml "pull_request_reviewers": [ "username1", "username2" ] ``` **Azure Repos:** crowdin.yml ```yml "pull_request_reviewers": [ "guid1", # user ID "guid2" # user ID ] ``` ## [Adding Labels to Source Strings](#adding-labels-to-source-strings) [Section titled “Adding Labels to Source Strings”](#adding-labels-to-source-strings) To add existing or new labels to the source strings, use the `labels` parameter. Labels will be added to the source strings only during the initial upload to the Crowdin project. The strings uploaded to the Crowdin project before the use of the `labels` parameter won’t be labeled. If you remove the label added during the initial upload directly in Crowdin, it won’t be re-added on the next syncs. Example: crowdin.yml ```yml "files": [ { "source" : "/resources/en/*.json", "translation" : "/resources/%two_letters_code%/%original_file_name%", "labels" : [ "android", "emails" ] } ] ``` Limitations Label names can contain any special character except `,`. Read more about [Labels](/project-settings/labels/). ## [Language Mapping](#language-mapping) [Section titled “Language Mapping”](#language-mapping) Often software projects have custom names for locale directories. Crowdin allows you to map your own languages to be recognizable in your projects. Let’s say your locale directories are named `en`, `uk`, `fr`, `de`. All of them can be represented by the `%two_letters_code%` placeholder. Still, you have one directory named `zh_CH`. You can also override language codes for other placeholders like `%android_code%`, `%locale%`, etc. Read more about [Language Mapping configuration for CLI](https://crowdin.github.io/crowdin-cli/advanced#languages-mapping-configuration). To make it work with Crowdin without changes in your project, you can set up Language Mapping via UI. * [Language mapping in Crowdin](/project-settings/export/) * [Language mapping in Crowdin Enterprise](/enterprise/project-settings/export/) ## [Using One Configuration File for VCS Integrations and CLI](#using-one-configuration-file-for-vcs-integrations-and-cli) [Section titled “Using One Configuration File for VCS Integrations and CLI”](#using-one-configuration-file-for-vcs-integrations-and-cli) There are cases when it’s necessary to use VCS integration and CLI for one project. Mostly, in this kind of situation, you’d need to have two separate configuration files, one for VCS integration and another for CLI. However, you can use a single configuration file for both cases. Since the VCS integration configuration file doesn’t contain `project_id` and `api_token` credentials required for CLI, you can pass them directly in the command using the following parameters: `-i/--project-id`, `-T/--token`. As a result, your command for downloading translations via CLI will look like the following: ```shell crowdin download -i {your-project-id} -T {your-token} ``` Alternatively, you may use [Environment Variables](#api-credentials-from-environment-variables) or [Split Project Configuration and API Credentials](https://crowdin.github.io/crowdin-cli/configuration#split-project-configuration-and-api-credentials). ## [Example Configurations](#example-configurations) [Section titled “Example Configurations”](#example-configurations) ##### [Uploading CSV files](#uploading-csv-files) [Section titled “Uploading CSV files”](#uploading-csv-files) crowdin.yml ```yml "project_id": "projectId" "api_token": "personal-access-token" "base_path": "." "base_url": "https://api.crowdin.com" "files": [ { "source": "/*.csv", "translation": "/%two_letters_code%/%original_file_name%", # Specifies whether first line should be imported or it contains columns headers "first_line_contains_header": true, # Used only when uploading CSV file to define data columns mapping "scheme": "identifier,source_phrase,translation,context,max_length" } ] ``` ##### [GetText Project](#gettext-project) [Section titled “GetText Project”](#gettext-project) crowdin.yml ```yml "project_id": "projectId" "api_token": "personal-access-token" "base_path": "." "base_url": "https://api.crowdin.com" "files" : [ { "source" : "/locale/en/**/*.po", "translation" : "/locale/%two_letters_code%/LC_MESSAGES/%original_file_name%", "languages_mapping" : { "two_letters_code" : { "zh-CN" : "zh_CH", "fr-QC": "fr" } } } ] ``` ##### [Android Project](#android-project) [Section titled “Android Project”](#android-project) crowdin.yml ```yml "project_id": "projectId" "api_token": "personal-access-token" "base_path": "." "base_url": "https://api.crowdin.com" "files" : [ { "source" : "/res/values/*.xml", "translation" : "/res/values-%android_code%/%original_file_name%", "languages_mapping" : { "android_code" : { "de" : "de" } } } ] ``` # Crowdin Query Language (CroQL) > Retrieve needed localization resources based on specific conditions Crowdin Query Language (CroQL) is a tool for Crowdin Editor and Crowdin Enterprise Editor and Crowdin and Crowdin Enterprise API that allows you to retrieve needed localization resources based on specific conditions. Using CroQL, you can filter source strings and their translations for a specific target language, TM segments, and Glossary Terms. You can use CroQL with the following API methods: * Crowdin * [List Strings](/developer/api/v2/#operation/api.projects.strings.getMany) * [List Strings](/developer/api/v2/string-based/#operation/api.projects.strings.getMany) String-based * [List Language Translations](/developer/api/v2/#operation/api.projects.languages.translations.getMany) * [List Language Translations](/developer/api/v2/string-based/#operation/api.projects.languages.translations.getMany) String-based * [List TM Segments](/developer/api/v2/#operation/api.tms.segments.getMany) * [List TM Segments](/developer/api/v2/string-based/#operation/api.tms.segments.getMany) String-based * [List Terms](/developer/api/v2/#tag/Glossaries/operation/api.glossaries.terms.getMany) * [List Terms](/developer/api/v2/string-based/#tag/Glossaries/operation/api.glossaries.terms.getMany) String-based * Crowdin Enterprise * [List Strings](/developer/enterprise/api/v2/#operation/api.projects.strings.getMany) * [List Strings](/developer/enterprise/api/v2/string-based/#operation/api.projects.strings.getMany) String-based * [List Language Translations](/developer/enterprise/api/v2/#operation/api.projects.languages.translations.getMany) * [List Language Translations](/developer/enterprise/api/v2/string-based/#operation/api.projects.languages.translations.getMany) String-based * [List TM Segments](/developer/enterprise/api/v2/#operation/api.tms.segments.getMany) * [List TM Segments](/developer/enterprise/api/v2/string-based/#operation/api.tms.segments.getMany) String-based * [List Terms](/developer/enterprise/api/v2/#tag/Glossaries/operation/api.glossaries.terms.getMany) * [List Terms](/developer/enterprise/api/v2/string-based/#tag/Glossaries/operation/api.glossaries.terms.getMany) String-based ## [Operators](#operators) [Section titled “Operators”](#operators) Main CroQL operators are listed below. Use and combine them to set specific conditions for retrieving the needed content from Crowdin. To form your CroQL query, you can use the elements from the tables below. ### [Arithmetic Operators](#arithmetic-operators) [Section titled “Arithmetic Operators”](#arithmetic-operators) The arithmetic operators are used to perform mathematic operations with any numeric data types. | Name | Symbol | Example | | -------------- | ------ | ------- | | Addition | + | 1 + 9 | | Subtraction | - | 11 - 1 | | Division | / | 20 / 2 | | Multiplication | \* | 2 \* 5 | | Negation | - | -10 | ### [Comparison Operators](#comparison-operators) [Section titled “Comparison Operators”](#comparison-operators) The comparison operators are used to compare values and return `true` or `false`. | Name | Symbol | Aliases | Example | | ---------------- | ---------------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------- | | Between | `{{expression}} between {{expression}} and {{expression}}` | | 5 between 1 and 10 | | Equal | = | | 10 = 10 | | Not equal | != | ≠ | 1 != 10; 1 ≠ 10 | | Greater | > | | 10 > 1 | | Greater or equal | >= | ≥ | 10 >= 1; 10 ≥ 1 | | Less | < | | 1 < 10 | | Less or equal | <= | ≤ | 1 <= 10; 1 ≤ 10 | | Contains | `{{string}} contains {{string}}` | | `"Hello World" contains "Hello"`; `"Hello World" contains text`; `text contains "Hello World"`; `context contains text` | ### [Logical Operators](#logical-operators) [Section titled “Logical Operators”](#logical-operators) The logical operators are used to combine multiple boolean expressions or values and provide a single boolean output. | Name | Symbol | Example | | ---- | ------ | ----------------- | | And | and | 1 < 10 and 10 > 1 | | Or | or | 1 < 10 or 10 > 1 | | Xor | xor | 1 < 10 xor 10 > 1 | | Not | not | not 1 < 10 | ### [Filtration Operators](#filtration-operators) [Section titled “Filtration Operators”](#filtration-operators) The filtration operators are used to filter the objects based on the specified condition. | Name | Symbol | Example | | ------ | ------------------------------------ | ----------------------------------------- | | Filter | `{{collection}} where {{predicate}}` | `translations where (count of votes > 0)` | | Match | `{{object}} with {{predicate}}` | `user with (login = "crowdin")` | ### [Conditional (Ternary) Operator](#conditional-ternary-operator) [Section titled “Conditional (Ternary) Operator”](#conditional-ternary-operator) The ternary operator is used to check a condition specified in the first value, and if it’s `true` it returns the second value, but if it’s `false` it returns the third value. | Name | Symbol | Example | | ------- | ---------------------------------------------------------- | -------------------------------------- | | Ternary | `If {{condition}} then {{expression}} else {{expression}}` | `If 1 < 10 then "less" else "greater"` | ### [Fetch Operators](#fetch-operators) [Section titled “Fetch Operators”](#fetch-operators) The fetch operators are used for retrieving data from the objects. | Name | Symbol | Example | | ---------- | ------------------------------------------ | ----------------------------------- | | Mention | `@user:{{string}}`; `@language:{{string}}` | `@user:"crowdin"`; `@language:"en"` | | Member | `{{member}} of {{object}}` | `count of translations` | | Identifier | `{{identifier}}` | `text`; `identifier` | ### [Scalar Operators](#scalar-operators) [Section titled “Scalar Operators”](#scalar-operators) The scalar operators are used to declare values for further processing. | Name | Symbol | Example | | -------- | -------------- | ------------------------------ | | Integer | `{{integer}}` | 10 | | Float | `{{float}}` | 10.01 | | String | `{{string}}` | ”crowdin” | | Datetime | `{{datetime}}` | ’today’; ‘2021-03-16 00:00:00’ | ### [Group Operator](#group-operator) [Section titled “Group Operator”](#group-operator) The group operator is used to determine the execution order of operators. | Name | Symbol | Example | | ----- | ------ | ------------------------------ | | Group | ( ) | 1 < 10 and (20 > 10 or 10 > 5) | ## [CroQL Query Examples](#croql-query-examples) [Section titled “CroQL Query Examples”](#croql-query-examples) In this section, you can find practical examples of CroQL queries that will help you understand and use the querying capabilities within Crowdin. These examples can help you learn how to create your own queries to retrieve specific sets of data based on various criteria, such as translation status, user activity, and string properties. [CroQL Tester - CroQL expression editor and tester ](https://store.crowdin.com/croql-tester)Try out your CroQL queries in the CroQL Tester. ### [Strings Queries](#strings-queries) [Section titled “Strings Queries”](#strings-queries) These queries are used to retrieve information about source strings. * Strings that have no Ukrainian translations with approvals or votes: ```graphql count of translations where ( language = @language:"uk" and ( count of approvals > 0 or count of votes > 0 ) ) = 0 ``` * Strings that have only one translation: ```graphql count of translations = 1 ``` * Strings that have translations from only one specific user: ```graphql count of translations > 0 and count of translations = count of translations where (user = @user:"crowdin") ``` * Strings that have at least one translation not from specific users: ```graphql count of translations where (user != @user:"crowdin") > 0 ``` * Strings that have all translations not from specific users: ```graphql count of translations > 0 and count of translations = count of translations where (user != @user:"crowdin") ``` * Strings filtered by identifier and numeric id of a file in your Crowdin project: ```graphql identifier = "key" and id of file = 777 ``` * Strings that have unresolved issues filtered by numeric id of a file in your Crowdin project: ```graphql id of file = 777 and count of comments where (has unresolved issue) > 0 ``` * Hidden strings that are not duplicates and have one or more translations: ```graphql is hidden and not is duplicate and count of translations > 0 ``` * Strings that have one or more approvals: ```graphql count of languages summary where (approvalsCount >= 1) > 0 ``` * Strings that have comments made by the user with a `crowdin` username: ```graphql count of comments where (user = @user:"crowdin") > 0 ``` * Strings that contain “ABC” in the source text but don’t contain “ABC” in their translations: ```graphql text contains "ABC" and (count of translations where (text contains "ABC") = 0) ``` ### [Translation Queries](#translation-queries) [Section titled “Translation Queries”](#translation-queries) These queries are used to retrieve information about translations. * Translations made by the user with a `crowdin` username or ones with ≥ 100 upvotes: ```graphql user = @user:"crowdin" or count of votes where ( is up ) >= 100 ``` ### [Translation Memory Segment Queries](#translation-memory-segment-queries) [Section titled “Translation Memory Segment Queries”](#translation-memory-segment-queries) These queries are used to retrieve information about TM segments. * Translation memory segments containing at least one record used one or more times: ```graphql count of records where (usageCount > 0) > 0 ``` * Translation memory segments containing at least one record created by the user with a `crowdin` username: ```graphql count of records where (createdBy = @user:"crowdin") > 0 ``` ### [Glossary Term Queries](#glossary-term-queries) [Section titled “Glossary Term Queries”](#glossary-term-queries) These queries are used to retrieve information about glossary terms. * Terms created by the user with a `crowdin` username for Ukrainian: ```graphql user = @user:"crowdin" and language = @language:"uk" ``` * Terms that contain “ABC” and have part of speech set to noun: ```graphql text contains "ABC" and partOfSpeech = "noun" ``` * Terms marked as not recommended or obsolete: ```graphql status = "not_recommended" or status = "obsolete" ``` * Terms that are of type abbreviation and have a note: ```graphql type = "abbreviation" and note contains "tooltip" ``` * Terms added after a specific date: ```graphql createdAt > '2024-12-01 00:00:00' ``` * Terms with lemma equal to “cancel” and in English: ```graphql lemma = "cancel" and language = @language:"en" ``` * Terms that include a reference URL: ```graphql url contains "https://" ``` ### [Examples based on the Editor Advanced Filter Options](#examples-based-on-the-editor-advanced-filter-options) [Section titled “Examples based on the Editor Advanced Filter Options”](#examples-based-on-the-editor-advanced-filter-options) #### [Strings](#strings) [Section titled “Strings”](#strings) * Strings added: ```graphql added between '2023-12-06 13:44:14' and '2023-12-07 13:44:14' ``` * Strings updated: ```graphql updated between '2023-12-06 13:44:14' and '2023-12-07 13:44:14' ``` * String Type: ```graphql type is plain or type is icu ``` * Comments: ```graphql count of comments > 0 ``` * Screenshots: ```graphql count of screenshots > 0 ``` * QA Issues: ```graphql count of languages summary where (has qa issues) > 0 ``` #### [Translations](#translations) [Section titled “Translations”](#translations) * Untranslated: ```graphql count of languages summary = 0 ``` * Partially translated (plurals): ```graphql count of languages summary where (is partially translated) > 0 ``` * Translated: ```graphql count of languages summary where (is translated) > 0 ``` * Translated by: ```graphql count of translations where (user = @user:"crowdin") > 0 ``` * Not Translated by: ```graphql count of translations where (user != @user:"crowdin") > 0 ``` * Same as source string: ```graphql count of languages summary where (has translation as source) > 0 ``` * Modified source String: ```graphql count of languages summary where (is source changed after translation) > 0 ``` * Translations updated: ```graphql count of languages summary where ( translation updated between '2023-12-06 13:44:14' and '2023-12-07 13:44:14') > 0 ``` * Votes: ```graphql count of languages summary where (rating > 0) > 0 ``` #### [Duplicates](#duplicates) [Section titled “Duplicates”](#duplicates) * Master strings: ```graphql not is duplicate ``` * Duplicates only: ```graphql is duplicate ``` * Duplicates with shared translations: ```graphql is duplicate and count of languages summary where (has shared translation) > 0 ``` * Duplicates with own translations: ```graphql is duplicate and count of languages summary where (not has shared translation and is translated) > 0 ``` #### [Approvals](#approvals) [Section titled “Approvals”](#approvals) * Translated, not approved: ```graphql count of languages summary where (is translated and not is approved) > 0 ``` * Partially approved (plurals): ```graphql count of languages summary where (is partially approved) > 0 ``` * Approved: ```graphql count of languages summary where (is approved) > 0 ``` * Approved by: ```graphql count of translations where (count of approvals where (user = @user:"crowdin") > 0) > 0 ``` * Not Approved by: ```graphql count of translations where (count of approvals where (user != @user:"crowdin") > 0) > 0 ``` * Has translations after approval: ```graphql count of languages summary where (has translation after approval) > 0 ``` #### [TM and MT](#tm-and-mt) [Section titled “TM and MT”](#tm-and-mt) * Translated by MT: ```graphql count of languages summary where (is translated by mt) > 0 ``` * Translated by TM: ```graphql count of languages summary where (is translated by tm) > 0 ``` * Translated by TM or MT: ```graphql count of languages summary where (is auto translated) > 0 ``` #### [Other](#other) [Section titled “Other”](#other) * Pre translation: * Used: Not available * Not used: Not available * Sort by: Not available * Verbal Expressions: Not available * Verbal Expression Scope: Not available ## [Context](#context) [Section titled “Context”](#context) CroQL can be used in the following contexts: source string context, translation context, and translation memory (TM) segment context. Use the following examples as a basis for building your CroQL queries. ### [Source String Context](#source-string-context) [Section titled “Source String Context”](#source-string-context) ```json { "type is plain": true, "type is plural": false, "type is icu": false, "type is asset": false, "text": "Quick Start", "identifier": "quick_start", "context": "quick_start", "max length": 0, "is visible": true, "is hidden": false, "is duplicate": false, "isPassedWorkflow": true, "file": { "id": 32, "name": "sample.csv", "title": "Sample", "type": "csv", "context": "Some useful context information" }, "branch": { "id": 7, "name": "main", "title": "Main" }, "comments": [ { "has issue": false, "has unresolved issue": false, "user": 1 } ], "screenshots": [], "translations": [ { "text": "Швидкий старт", } ], "languages summary": [ { "language": "en", } ], "labels": [ { "id": 1, "title": "label title", "is system": false } ], "added": "2021-04-08 12:33:27", "updated": "2021-04-08 12:33:27" } ``` | | | | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type is plain` | **Type:** `boolean`**Description:** The source string with plain text. | | `type is plural` | **Type:** `boolean`**Description:** The source string with plural forms. | | `type is icu` | **Type:** `boolean`**Description:** The source string with ICU. | | `type is asset` | **Type:** `boolean`**Description:** The source string is an asset. | | `text` | **Type:** `string`**Description:** The source string text. | | `identifier` | **Type:** `string`**Description:** The source string identifier (key). | | `context` | **Type:** `string`**Description:** The source string context. | | `max length` | **Type:** `integer`**Description:** The source string max.length. | | `is visible` | **Type:** `boolean`**Description:** The source string is visible. | | `is hidden` | **Type:** `boolean`**Description:** The source string is hidden. | | `is duplicate` | **Type:** `boolean`**Description:** The source string is duplicate. | | `isPassedWorkflow` | **Type:** `boolean`**Description:** Crowdin Enterprise only. The source string passed through a project workflow. | | `file` | **Type:** `object`**Description:** The source string file. | | `branch` | **Type:** `object`**Description:** The source string branch. | | `comments` | **Type:** `array`**Description:** The source string comments. | | `has issue` | **Type:** `boolean`**Description:** The source string has an issue. | | `has unresolved issue` | **Type:** `boolean`**Description:** The source string has an unresolved issue. | | `user` | **Type:** `integer`**Description:** Numeric identifier of the user who added a comment. | | `screenshots` | **Type:** `array`**Description:** The source string screenshots. | | `translations` | **Type:** `array`**Description:** Translations of the source string. | | `text` | **Type:** `string`**Description:** Translation text. | | `plural form` | **Type:** `string`**Description:** Translation plural form. | | `is pre translated` | **Type:** `boolean`**Description:** Translation added via pre-translation. | | `provider` | **Type:** `string`**Allowed values:** `tm`, `global_tm`, `google`, `google_automl`, `microsoft`, `crowdin`, `deepl`, `modernmt`, `amazon`, `watson`, `custom_mt`**Description:** Translation provided via translation memory or machine translation engine. | | `language` | **Type:** `string`**Description:** Language identifier specified as a string. Use the [language codes](/developer/language-codes/), for example, `“uk”` for Ukrainian. To specify in queries, use the format: `@language:“uk”`. | | `user` | **Type:** `integer`**Description:** Numeric identifier of the user who added a translation. | | `votes` | **Type:** `array`**Description:** Array of the votes added to the translation. | | `is up` | **Type:** `boolean`**Description:** Upvote. | | `is down` | **Type:** `boolean`**Description:** Downvote. | | `user` | **Type:** `integer`**Description:** Numeric identifier of the user who added a vote for translation. | | `added` | **Type:** `datetime`**Description:** Date when a vote for translation was added. | | `approvals` | **Type:** `array`**Description:** Array of the added translation approvals. | | `user` | **Type:** `integer`**Description:** Numeric identifier of the user who approved a translation. | | `added` | **Type:** `datetime`**Description:** Date when a translation approval was added. | | `updated` | **Type:** `datetime`**Description:** Date when a translation was updated. | | `languages summary` | **Type:** `array`**Description:** The source string top translations (the translations with the highest priority). | | `language` | **Type:** `string`**Description:** Language identifier specified as a string. Use the [language codes](/developer/language-codes/), for example, `“uk”` for Ukrainian. To specify in queries, use the format: `@language:“uk”`. | | `is translated` | **Type:** `boolean`**Description:** The source string is translated. | | `is partially translated` | **Type:** `boolean`**Description:** The source string is partially translated. | | `is approved` | **Type:** `boolean`**Description:** The source string is approved. | | `is partially approved` | **Type:** `boolean`**Description:** The source string is partially approved. | | `translation updated` | **Type:** `boolean`**Description:** The source string translation is updated. | | `is auto translated` | **Type:** `boolean`**Description:** The source string is translated by TM or MT. | | `is translated by tm` | **Type:** `boolean`**Description:** The source string is translated by TM. | | `is translated by mt` | **Type:** `boolean`**Description:** The source string is translated by MT. | | `is source changed after translation` | **Type:** `boolean`**Description:** The source string changed after translation. | | `has translation as source` | **Type:** `boolean`**Description:** The source string has translation equal to source text. | | `has translation after approval` | **Type:** `boolean`**Description:** The source string has translation after approval. | | `has shared translation` | **Type:** `boolean`**Description:** The source string duplicate has shared translations from a master string. | | `has qa issues` | **Type:** `boolean`**Description:** The source string has QA issues. | | `has empty translation qa issues` | **Type:** `boolean`**Description:** The source string has empty translation QA issues. | | `has translation length qa issues` | **Type:** `boolean`**Description:** The source string has translation length QA issues. | | `has tags mismatch qa issues` | **Type:** `boolean`**Description:** The source string has tags mismatch QA issues. | | `has spaces mismatch qa issues` | **Type:** `boolean`**Description:** The source string has spaces mismatch QA issues. | | `has variables mismatch qa issues` | **Type:** `boolean`**Description:** The source string has variables mismatch QA issues. | | `has punctuation mismatch qa issues` | **Type:** `boolean`**Description:** The source string has punctuation mismatch QA issues. | | `has character case mismatch qa issues` | **Type:** `boolean`**Description:** The source string has character case mismatch QA issues. | | `has special characters mismatch qa issues` | **Type:** `boolean`**Description:** The source string has special characters mismatch QA issues. | | `has incorrect translation qa issues` | **Type:** `boolean`**Description:** The source string has incorrect translation QA issues. | | `has spelling qa issues` | **Type:** `boolean`**Description:** The source string has spelling QA issues. | | `has icu syntax qa issues` | **Type:** `boolean`**Description:** The source string has ICU syntax QA issues. | | `has terms qa issues` | **Type:** `boolean`**Description:** The source string has terms QA issues. | | `has duplicate translation qa issues` | **Type:** `boolean`**Description:** The source string has duplicate translation QA issues. | | `has ftl syntax qa issues` | **Type:** `boolean`**Description:** The source string has FTL syntax QA issues. | | `has android syntax qa issues` | **Type:** `boolean`**Description:** The source string has Android syntax QA issues. | | `has custom qa issues` | **Type:** `boolean`**Description:** The source string has Custom QA issues. | | `rating` | **Type:** `integer`**Description:** The source string translation rating. | | `approvalsCount` | **Type:** `integer`**Description:** The number of translation approvals. | | `labels` | **Type:** `array`**Description:** The source string labels. | | `id` | **Type:** `integer`**Description:** Numeric identifier of the label. | | `title` | **Type:** `string`**Description:** Label title. | | `is system` | **Type:** `boolean`**Description:** System label (label with source file name that is automatically added to strings in string-based projects). | | `added` | **Type:** `datetime`**Description:** Date when a source string was added. | | `updated` | **Type:** `datetime`**Description:** Date when a source string was updated. | ### [Translation Context](#translation-context) [Section titled “Translation Context”](#translation-context) ```json { "text": "Швидкий старт", "plural form": "none", "is pre translated": true, "provider": "tm", "user": 1, "votes": [ { "is up": true, "is down": false, "user": 2, "added": "2021-04-09 13:44:14" } ], "approvals": [ { "user": 2, "added": "2021-04-09 13:44:14" } ], "updated": "2021-04-09 10:23:17" } ``` | | | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `text` | **Type:** `boolean`**Description:** Translation text. | | `plural form` | **Type:** `string`**Description:** Translation plural form. | | `is pre translated` | **Type:** `boolean`**Description:** Translation added via pre-translation. | | `provider` | **Type:** `string`**Allowed values:** `tm`, `global_tm`, `google`, `google_automl`, `microsoft`, `crowdin`, `deepl`, `modernmt`, `amazon`, `watson`, `custom_mt`**Description:** Translation provided via translation memory or machine translation engine. | | `user` | **Type:** `integer`**Description:** Numeric identifier of the user who added a translation. | | `votes` | **Type:** `array`**Description:** Array of the votes added to the translation. | | `is up` | **Type:** `boolean`**Description:** Upvote. | | `is down` | **Type:** `boolean`**Description:** Downvote. | | `user` | **Type:** `integer`**Description:** Numeric identifier of the user who added a vote for translation. | | `added` | **Type:** `datetime`**Description:** Date when a vote for translation was added. | | `approvals` | **Type:** `array`**Description:** Array of the added translation approvals. | | `user` | **Type:** `integer`**Description:** Numeric identifier of the user who approved a translation. | | `added` | **Type:** `datetime`**Description:** Date when a translation approval was added. | | `updated` | **Type:** `datetime`**Description:** Date when a translation was updated. | ### [Translation Memory (TM) Segment Context](#translation-memory-tm-segment-context) [Section titled “Translation Memory (TM) Segment Context”](#translation-memory-tm-segment-context) ```json { "records": [ { "id": 1, "text": "Перекладений текст", "usageCount": 77, "createdBy": 1, "updatedBy": 1, "createdAt": "2027-09-16T13:48:04+00:00", "updatedAt": "2027-09-16T13:48:04+00:00" } ] } ``` | | | | ------------ | ----------------------------------------------------------------------------------------------------------- | | `records` | **Type:** `array`**Description:** Array of translation memory segment records. | | `id` | **Type:** `integer`**Description:** Numeric identifier of a record. | | `text` | **Type:** `string`**Description:** Translation text of a record. | | `usageCount` | **Type:** `integer`**Description:** The number of times a translation memory record has been used. | | `createdBy` | **Type:** `integer`**Description:** Numeric identifier of the user who created a translation memory record. | | `updatedBy` | **Type:** `integer`**Description:** Numeric identifier of the user who updated a translation memory record. | | `createdAt` | **Type:** `datetime`**Description:** Date when a translation memory record was created. | | `updatedAt` | **Type:** `datetime`**Description:** Date when a translation memory record was updated. | ### [Glossary Concept Context](#glossary-concept-context) [Section titled “Glossary Concept Context”](#glossary-concept-context) ```json { "id": 101, "user": 12, "subject": "User Interfact", "definition": "A command used to save user progress in the application.", "url": "https://example.com/concept/save", "note": "Commonly used in forms and toolbars.", "translatable": true, "createdAt": "2027-09-16T13:48:04+00:00", "updatedAt": "2027-09-16T13:48:04+00:00" } ``` | | | | -------------- | -------------------------------------------------------------------------------------------------------------------------- | | `id` | **Type:** `integer`**Description:** Numeric identifier of the glossary concept. | | `user` | **Type:** `integer`**Description:** Numeric identifier of the user who created the concept. | | `subject` | **Type:** `string`**Description:** Domain or area of knowledge the concept belongs to (e.g., UI, development, healthcare). | | `definition` | **Type:** `string`**Description:** General explanation or definition of the concept. | | `url` | **Type:** `string`**Description:** URL linking to a resource with more information about the concept. | | `note` | **Type:** `string`**Description:** Additional information or clarification for translators. | | `translatable` | **Type:** `boolean`**Description:** Indicates whether the concept can be translated into other languages. | | `createdAt` | **Type:** `datetime`**Description:** Date and time when the concept was created. | | `updatedAt` | **Type:** `datetime`**Description:** Date and time when the concept was last updated. | ### [Glossary Term Context](#glossary-term-context) [Section titled “Glossary Term Context”](#glossary-term-context) ```json { "id": 301, "text": "Cancel", "description": "A command to stop or abort an operation.", "language": "en", "user": 42, "partOfSpeech": "verb", "status": "admitted", "type": "abbreviation", "gender": "neuter", "note": "Often used in confirmation dialogs.", "lemma": "cancel", "url": "https://example.com/term/cancel", "concept": 101, "createdAt": "2027-09-16T13:48:04+00:00", "updatedAt": "2027-09-16T13:48:04+00:00" } ``` | | | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | **Type:** `integer`**Description:** Numeric identifier of the glossary term. | | `text` | **Type:** `string`**Description:** The term itself, in the specified language. | | `description` | **Type:** `string`**Description:** Additional explanation or meaning of the term. | | `language` | **Type:** `string`**Description:** Language identifier specified as a string. Use the [language codes](/developer/language-codes/), for example, `“uk”` for Ukrainian. To specify in queries, use the format: `@language:“uk”`. | | `user` | **Type:** `integer`**Description:** Numeric identifier of the user who added the term. | | `partOfSpeech` | **Type:** `string`**Allowed values:** `noun`, `verb`, `adj`, `pron`, `propn`, `det`, `adv`, `adp`, `cconj`, `sconj`, `num`, `intj`, `aux`, `prt`, `sym`, `x`**Description:** The grammatical category of the term. | | `status` | **Type:** `string`**Allowed values:** `preferred`, `admitted`, `not_recommended`, `obsolete`, `draft`**Description:** Indicates the approval or usage level of the term. | | `type` | **Type:** `string`**Allowed values:** `full_form`, `acronym`, `abbreviation`, `short_form`, `phrase`, `variant`**Description:** Classification of the term by structure or usage. | | `gender` | **Type:** `string`**Allowed values:** `masculine`, `feminine`, `neuter`, `common`, `other`**Description:** Grammatical gender of the term, if applicable. | | `note` | **Type:** `string`**Description:** Additional translator guidance or term-related notes. | | `lemma` | **Type:** `string`**Description:** The base form of the term. | | `url` | **Type:** `string`**Description:** Reference link for the term. | | `concept` | **Type:** `integer`**Description:** ID of the concept the term belongs to. | | `createdAt` | **Type:** `datetime`**Description:** Date and time the term was created. | | `updatedAt` | **Type:** `datetime`**Description:** Date and time the term was last updated. | # About Crowdin Apps > Join the growing localization management platform! Build apps for all the teams already using Crowdin or Crowdin Enterprise to customize and extend localization experience. ![Crowdin Developer](/_astro/developer-portal-hero.BhGVcJKs_ZQCktL.webp) 3M+ Registered users 200k+ Projects 15k+ Active project owners By creating Crowdin apps, developers can integrate existing services with Crowdin, add new features, upload and manage content. Crowdin apps are web applications that function remotely via HTTP. To an end user, an app appears as a fully integrated part of Crowdin. Once your app is installed, its features are delivered straight to the Crowdin UI. You can develop a Crowdin app using any of the preferred programming languages and web frameworks, and deploy it in many different ways. From massive SaaS services to static apps served right from a code repo, Crowdin apps are designed to let you connect anything to Crowdin. ![Integrating with Crowdin Diagram](/_astro/integrating-crowdin-apps.BANRzbJN_2idEYz.svg) ## [Creating Crowdin Apps](#creating-crowdin-apps) [Section titled “Creating Crowdin Apps”](#creating-crowdin-apps) The development of Crowdin App starts with creating an app descriptor. The app descriptor is a JSON file that describes the interaction of the app with Crowdin. The descriptor includes general information for the app, as well as the modules that the app will be using or extending. Basically, the descriptor is a middle ground between the remote app and Crowdin. When a Crowdin account owner installs an app, what they are really doing is installing this descriptor file, which contains pointers to your app. Read more about [App Descriptor](/developer/crowdin-apps-app-descriptor/). The next step would be to implement the app functionality according to the app descriptor which implies the following steps: Step 1 Event listeners implementation – the usage of webhooks which are triggered by Crowdin to perform certain actions on the app side (installation of the app, app removal, etc). Step 2 Modules implementation – module usage in the Crowdin apps. Modules are the functional parts integrated into the apps with help of which apps extend Crowdin and interact with it. Read more about [UI Modules](/developer/crowdin-apps-modules-ui/) and [File Processing Modules](/developer/crowdin-apps-modules-file-processing/). ## [Using Crowdin APIs in Crowdin Apps](#using-crowdin-apis-in-crowdin-apps) [Section titled “Using Crowdin APIs in Crowdin Apps”](#using-crowdin-apis-in-crowdin-apps) Crowdin Apps communicate with Crowdin using our RESTful APIs. You can use the Crowdin APIs in Crowdin apps you develop for Crowdin, as well as in scripts, API clients, or other methods of making calls. Our APIs allow you to manage Crowdin TMs, glossaries, source content (files and strings), translations, branches, etc. You can use the APIs to upload source files, export translations, as well as for user management, generating reports, and more. [API Overview ](/developer/api/)Explore the Crowdin API documentation to learn more about the available endpoints and how to use them. Need Help? We support all developers who help us improve our product and add interesting developments to our community. [Contact Support ](https://crowdin.com/contacts) ## [Using JS API in Crowdin Apps](#using-js-api-in-crowdin-apps) [Section titled “Using JS API in Crowdin Apps”](#using-js-api-in-crowdin-apps) For improved interaction between the Crowdin app and Crowdin, you can use our library that provides cross-window communication. The library simplifies the interaction with the Crowdin interface, allows you to get additional information from the page where the application was opened, or manipulate certain UI elements of the page directly from the application. Read more about [Crowdin Apps JS](/developer/crowdin-apps-js/). ## [Publishing Your App](#publishing-your-app) [Section titled “Publishing Your App”](#publishing-your-app) After creating and testing your app, the next thing you need to do is to publish it into the cloud or any public server so that it’s always accessible to Crowdin and other users. Read more about [Publishing Your App](/developer/crowdin-apps-publishing/). When you’re ready to share your app you can submit your app to the [Crowdin Store](https://store.crowdin.com). This allows other users to install and run your app(s). ## [Examples of Crowdin Apps](#examples-of-crowdin-apps) [Section titled “Examples of Crowdin Apps”](#examples-of-crowdin-apps) Before you start developing your own Crowdin apps, you can take a look at a few examples. They showcase the integration between Crowdin Enterprise and external services like Mailchimp and SendGrid. Read more about [Crowdin Mailchimp Example](https://github.com/crowdin-community/crowdin-mailchimp-example) and [Crowdin SendGrid Example](https://github.com/crowdin-community/crowdin-sendgrid-example). ## [Assistance from Our Team](#assistance-from-our-team) [Section titled “Assistance from Our Team”](#assistance-from-our-team) Our team is ready to help you with the technical implementation of your app. Once your app is ready we’ll discuss how we can help you with exposure to our customers. For any guidance from our team contact us at [](mailto:support@crowdin.com). ## [Case Study](#case-study) [Section titled “Case Study”](#case-study) # App Descriptor > Learn how to define the app descriptor The app descriptor is one of the essential building blocks of Crowdin apps. The app descriptor is a JSON file (for example, `manifest.json`) that includes general information for the app, as well as the modules that the app wants to operate with or extend. It describes how the application will work, what resources will be used, etc. ## [App Descriptor Structure](#app-descriptor-structure) [Section titled “App Descriptor Structure”](#app-descriptor-structure) The app descriptor is a JSON object with the following structure: manifest.json ```json { "identifier": "your-application-identifier", "name": "Your Application", "description": "Application description", "logo": "/assets/logos/app-logo.png", "baseUrl": "http://example.com", "authentication": { "type": "crowdin_app", "clientId": "your-client-id" }, "events": { "installed": "/hooks/installed" }, "scopes": [ "project" ], "modules": { "project-integrations": [ { "key": "your-module-key", "name": "Module Name", "description": "Module description", "logo": "/assets/logos/module-logo.png", "url": "/page/integration", "environments": [ "crowdin", "crowdin-enterprise" ] } ] } } ``` | Property | Description | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `identifier` | **Type:** `string` (`^[a-z0-9-._]+$`)**Required:** yes**Description:** A unique key to identify the app. This identifier must be <= 255 characters.CautionDon’t use uppercase in the app identifier. | | `name` | **Type:** `string`**Required:** yes**Description:** The human-readable name of the app. | | `baseUrl` | **Type:** `string` (`uri`)**Required:** yes**Description:** The base URL of the remote app, which is used for all communications back to the app instance. Once the app is installed in a workspace, the app’s baseUrl can’t be changed without uninstalling the app beforehand.**This is important:** choose your baseUrl wisely before making your app public. The baseUrl must start with `https://` to ensure that all data is sent securely between our cloud instances and your app. | | `authentication` | **Type:** `Authentication`**Required:** yes**Description:** Specifies the authentication type to use when signing requests between the host application and the Crowdin app. | | `description` | **Type:** `string`**Description:** The human-readable description of what the app does. The description will be visible in the Crowdin UI. | | `logo` | **Type:** `string` (`relativeUri`)**Description:** The image URL relative to the app’s base URL which will be displayed in the Crowdin UI. | | `events` | **Type:** `Events`**Description:** Allow the app to register for app event notifications. | | `scopes` | **Type:** \[`string`, … ]**Description:** Set of [scopes](/developer/understanding-scopes/) requested by this app.```json "scopes": [ "project", "tm" ] ``` | | `modules` | **Type:** `object`**Description:** The list of modules this app provides. | | `environments` | **Type:** \[`string`, … ]**Allowed values:** `crowdin`, `crowdin-enterprise`**Description:** Set of environments where a module could be installed.```json "environments": [ "crowdin-enterprise" ] ``` | ## [Authentication](#authentication) [Section titled “Authentication”](#authentication) Specifies the authentication type to use when signing requests from the host application to the Crowdin app. Crowdin Apps support two types of authentication: * using OAuth app (`crowdin_app` value) * without OAuth app (`none` value) In case your Crowdin app requires access to Crowdin API at any time, it’s recommended to use the `crowdin_app`, in other cases feel free to use the `none`. The authentication type `none` grants access to Crowdin API as well as the `crowdin_app`, but only when the Crowdin app is executed on the user side, for example, when the iframe opens. Example: manifest.json ```json { "authentication": { "type": "crowdin_app", "clientId": "your-client-id" } } ``` | Property | Description | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `type` | **Type:** `string`**Defaults to:** `none`**Allowed values:** `none`, `crowdin_app`**Description:** The type of authentication to use. | | `clientId` | **Type:** `string`**Description:** OAuth client id for authorization via the `crowdin_app` type. | ## [Modules](#modules) [Section titled “Modules”](#modules) Modules are how apps extend Crowdin and interact with it. Using modules your app can do the following things: * Extend the Crowdin UI. * Create integrations with external services. * Add support for new custom file formats. * Customize processing for supported file formats. Read more about [UI Modules](/developer/crowdin-apps-modules-ui/) and [File Processing Modules](/developer/crowdin-apps-modules-file-processing/). ## [Events](#events) [Section titled “Events”](#events) Allow an app to register callbacks for events that occur in the workspace. When an event is fired, a POST request will be made to the appropriate URL registered for the event. The installed callback is an integral part of the installation process of an app, whereas the remaining events are essentially webhooks. Each property within this object is a URL relative to the app’s base URL. Example: manifest.json ```json { "events": { "installed": "/hook/installed", "uninstall": "/hook/uninstall" } } ``` | Property | Description | | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `installed` | **Type:** `string`**Description:** The event that is sent to an app after a user installed the app in Crowdin.This event is required if you use `crowdin_app`. Read more about [Authentication](#authentication). | | `uninstall` | **Type:** `string`**Description:** The event that is sent to an app before the app uninstallation from Crowdin. | ### [Installed Event Payload](#installed-event-payload) [Section titled “Installed Event Payload”](#installed-event-payload) The Installed event is sent from Crowdin to the remote app when a user installs the app in Crowdin. The Installed event contains the information about the Crowdin workspace or profile the Crowdin App was installed to, the information about the app itself, as well as the credentials to fetch an API token. Read more about [Installed Event Flow](/developer/crowdin-apps-installation/#installed-event-communication-flow). Payload example: ```json { "appId": "your-application-identifier", "appSecret": "dbfg....asdffgg", "clientId": "your-client-id", "userId": 1, "organizationId": 1, "domain": null, "baseUrl": "https://crowdin.com" } ``` ```json { "appId": "your-application-identifier", "appSecret": "dbfg....asdffgg", "clientId": "your-client-id", "userId": 1, "organizationId": 1, "domain": "{domain}", "baseUrl": "https://{domain}.crowdin.com" } ``` Properties: | Property | Description | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `appId` | **Type:** `string`**Description:** The identifier of the app that is declared in the app descriptor file. | | `appSecret` | **Type:** `string`**Description:** The unique secret used for authorization of your Crowdin app. | | `clientId` | **Type:** `string`**Description:** The OAuth client identifier that is declared in the app descriptor file. | | `userId` | **Type:** `integer`**Description:** The numeric identifier of the user that installed the app in Crowdin Enterprise. | | `organizationId` | **Type:** `integer`**Description:** The numeric identifier of the organization the app was installed to. | | `domain` | **Type:** `string`**Description:** The name of the organization in Crowdin Enterprise the app was installed to. For Crowdin the domain value is always null | | `baseUrl` | **Type:** `string`**Description:** The `baseUrl` of the organization in Crowdin Enterprise the app was installed to. For Crowdin the `baseUrl` value is always `https://crowdin.com` | ### [Uninstall Event Payload](#uninstall-event-payload) [Section titled “Uninstall Event Payload”](#uninstall-event-payload) The uninstall event is sent from Crowdin Enterprise to the remote Crowdin app when a user uninstalls the app from Crowdin Enterprise. The Uninstall event, like the install event, contains the information about the Crowdin workspace or account the Crowdin App was installed to, and the information about the app itself. After receiving the uninstall event, it’s necessary to find and remove all of the data related to the Crowdin workspace or account the app is removed from. Payload example: ```json { "appId": "your-application-identifier", "clientId": "your-client-id", "organizationId": 1, "domain": null, "baseUrl": "https://crowdin.com" } ``` ```json { "appId": "your-application-identifier", "clientId": "your-client-id", "organizationId": 1, "domain": "{domain}", "baseUrl": "https://{domain}.crowdin.com" } ``` Properties: | Property | Description | | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `appId` | **Type:** `string`**Description:** The identifier of the app that is declared in the app descriptor file. | | `clientId` | **Type:** `string`**Description:** The OAuth client identifier that is declared in the app descriptor file. | | `organizationId` | **Type:** `integer`**Description:** The numeric identifier of the organization the app uninstalled from. | | `domain` | **Type:** `string`**Description:** The name of the organization in Crowdin Enterprise the app uninstalled from. For Crowdin the domain value is always null | | `baseUrl` | **Type:** `string`**Description:** The baseUrl of the organization in Crowdin Enterprise the app uninstalled from. For Crowdin the `baseUrl` value is always `https://crowdin.com` | # App Installation > Learn how to install and configure Crowdin Apps from the Crowdin Store or manually You can install Crowdin Apps either from the [Crowdin Store](https://store.crowdin.com) or manually, depending on whether the app is already published or not. ## [Installation in Crowdin](#installation-in-crowdin) [Section titled “Installation in Crowdin”](#installation-in-crowdin) To install the app that is already published on the Crowdin Store, follow these steps: 1. Open your profile home page and select **Store** on the left sidebar. 2. Click **Install** on the needed app. ![Store](/_astro/crowdin_apps_store_install.qhOH9Qnj_Z1OE1Y7.webp) 3. In the appeared dialog, configure preferred permissions and click **Install**. To install a private app, follow these steps: 1. Go to **Account Settings > Apps** and click **Install Private App**. 2. In the appeared dialog, paste in the Crowdin app Manifest URL and click **Install**. 3. In the **Install Application** dialog, configure preferred permissions and click **Install**. ## [Installation in Crowdin Enterprise](#installation-in-crowdin-enterprise) [Section titled “Installation in Crowdin Enterprise”](#installation-in-crowdin-enterprise) To install the app that is already published on the Crowdin Store, follow these steps: 1. Open your organization’s **Workspace** and select **Store** on the left sidebar. 2. Click **Install** on the needed app. ![Store](/_astro/crowdin_apps_store_install_e.l17BOZxz_1eJdSr.webp) 3. In the appeared dialog, configure preferred permissions and click **Install**. To install a private app, follow these steps: 1. Go to **Organization Settings > Apps** and click **Install Private App**. 2. In the appeared dialog, paste in the Crowdin app Manifest URL and click **Install**. 3. In the **Install Application** dialog, configure preferred permissions and click **Install**. ## [Crowdin Apps Permission Configuration](#crowdin-apps-permission-configuration) [Section titled “Crowdin Apps Permission Configuration”](#crowdin-apps-permission-configuration) Configure preferred permissions for each app during the installation process. This step allows you to define who can access and use the app across its various modules and specifying in which projects of your Crowdin account (for Crowdin) or Crowdin organization (for Crowdin Enterprise) it can be used. If you restrict access to certain projects by using the **Selected projects** option, the app will not be able to communicate via the API with projects that are not included in the selected list. Also, the app will only be displayed in the UI of the selected projects. This ensures that the app’s functionality and access are precisely tailored to the specific needs and security requirements of your organization. You can configure these access permissions at the time of installation or adjust them at any time for already installed apps. This flexibility allows you to respond to changes in your project requirements or security policies by updating the access settings to either expand or restrict the app’s functionality and visibility within your Crowdin projects. ### [User Access Categories](#user-access-categories) [Section titled “User Access Categories”](#user-access-categories) You can define which user categories are allowed to use the app. This setting is applied to each app module independently. Available options for Crowdin: * Only me (i.e., project owner) * Me, project managers and developers * All project members * Custom Access (selected users) * Guests (unauthenticated users) ![Permissions](/_astro/crowdin_apps_app_permissions.BZ0ukKzs_Z1cWJOi.webp) Available options for Crowdin Enterprise: * Only organization admins * Organization admins, project managers and developers * All project members * Custom Access (selected users) * Guests (unauthenticated users) ![Permissions](/_astro/crowdin_apps_app_permissions_e.Dj_MpUOT_108fGQ.webp) ### [Project Access Configuration](#project-access-configuration) [Section titled “Project Access Configuration”](#project-access-configuration) In addition to user access, you can also specify the projects in which the app can be used (these settings apply across all app modules). Project access options: * Projects you own (for Crowdin) or All projects (for Crowdin Enterprise) * Selected projects This targeted approach allows for enhanced security and customization, ensuring that the app is only used where it’s really needed. ## [Installed Event Communication Flow](#installed-event-communication-flow) [Section titled “Installed Event Communication Flow”](#installed-event-communication-flow) When a Crowdin App is installed in the Account Settings the authorization flow takes place during which Crowdin and Crowdin App exchange the authorization data (the authorization code is being exchanged for an access token). In the following illustration, you can see the events that take place during this process. ![Communication between Crowdin and Crowdin App](/_astro/crowdin_apps_communication_crowdin.BzxfDlMY_YxXot.webp) Let’s examine in detail each step that happens in the illustration: 1. Installation of the Manifest URL - the user pastes in the Manifest URL in the *Account Settings* > *Crowdin Applications* and clicks **Install**. 2. Fetching content from the Manifest URL - the request is sent to Crowdin App. 3. Response: manifest JSON - Crowdin App returns the Manifest JSON that contains the data about the app. 4. Manifest Data Validation - the received content is validated according to the structure and data of the Manifest JSON. 5. Prompt to install - the information about the Crowdin App, as well as the list of permissions and the **Install** button is displayed to the user. 6. Confirmation of the installation - the user confirms the installation of the Crowdin App. 7. The Installed event - Crowdin sends the Installed event with the authorization code to the Crowdin App for API token generation. 8. Token request - Crowdin App sends the request for API token acquiring: ```plaintext POST https://accounts.crowdin.com/oauth/token ``` 9. Access Token - authorization service returns the API access token and the refresh token. 10. Success Code Response - Crowdin App returns the success status code (2xx) which confirms that the application installation was successfully finished. In case the status code is different, the application will be removed from the Crowdin account. **Token request parameters (step 8):** | | | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `grant_type: crowdin_app` | **Type:** `string`**Required:** yes**Description:** The parameter is used for the flow specification of an OAuth app. | | `client_id` | **Type:** `string`**Required:** yes**Description:** Client ID for the app is received when the app is registered. | | `client_secret` | **Type:** `string`**Required:** yes**Description:** Client Secret for the app is received when the app is registered. | | `app_id` | **Type:** `string`**Required:** yes**Description:** Crowdin app identifier from the app descriptor. | | `app_secret` | **Type:** `string`**Required:** yes**Description:** The unique secret used for authorization of your Crowdin app. This value is retrieved from the installed event. | | `domain` | **Type:** `string\|null`**Required:** yes**Description:** The name of the organization from which the app is accessed. This value is retrieved from the installed event. | | `user_id` | **Type:** `integer`**Required:** yes**Description:** The identifier of the user who installed the app. This value is retrieved from the installed event. | # Crowdin Apps JS > Utilize the Crowdin Apps JS library to interact with Crowdin The Crowdin Apps JS is a library built for easier interaction with Crowdin. Since the integration uses the iframes, there are certain limitations on the interaction between windows: environments are encapsulated and do not have access to the page content, code, styles, and any other information related to the page. The Crowdin Apps JS library is based on cross-window messaging technology and uses *postMessage()* to interact between multiple windows. ## [Getting Started](#getting-started) [Section titled “Getting Started”](#getting-started) To use this library, connect it on the module page by inserting the following code into the page header: ```html ``` After adding the code mentioned above, you’ll be able to use the global AP variable. Caution Don’t download the `iframe.js` file and serve it up from your app server directly. The `iframe.js` file must be served up by Crowdin for establishing of the cross-domain messaging bridge. The `iframe.js` file is only intended for use in an iframe inside Crowdin and does not work for standalone web pages. ## [Global Actions](#global-actions) [Section titled “Global Actions”](#global-actions) Global actions are the actions that are available on all module pages. ### [AP.getContext(callback)](#get-context) [Section titled “AP.getContext(callback)”](#get-context) The method retrieves the context information about the current page and puts it into the callback. For example, the project ID where the module is opened or the locale used on the page. Example: ```js AP.getContext(function(contextData) { console.log("Object: context", contextData); }); ``` Properties: | | | | ---------- | ----------------------------------------------------------------------------------- | | `callback` | **Type:** `function`**Description:** The callback that handles the response object. | Depending on the current page on which this method is used, the payload may contain a different set of the context information. Payload example: ```json { "project_id": "12", "organization_id": "200000000", "editor": { "mode": "translate", "theme": "dark", "source_language_id": "en", "target_language_id": "fr", "file": 12345, "fileData": { }, "workflow_step": { "id": 7777, "title": "Translation", "type": "Translate" } } } ``` Properties: | | | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `project_id` | **Type:** `string`**Description:** The identifier of the Crowdin project. | | `organization_id` | **Type:** `string`**Description:** Crowdin Enterprise only. The identifier of the organization in Crowdin Enterprise. | | `editor` | **Type:** `object`**Description:** Contains settings and context for the Editor. | | `editor.mode` | **Type:** `string`**Description:** Active mode of the Editor. | | `editor.theme` | **Type:** `string`**Description:** Active theme of the Editor. | | `editor.source_language_id` | **Type:** `string`**Description:** The identifier of the source language. | | `editor.target_language_id` | **Type:** `string`**Description:** The identifier of the target language. | | `editor.file` | **Type:** `integer`**Description:** The numeric identifier of a file in the Crowdin project that is open in the Editor. | | `editor.fileData` | **Type:** `object`**Description:** Contains data related to a file in the Crowdin project that is open in the Editor. | | `editor.workflow_step` | **Type:** `object`**Description:** Crowdin Enterprise only. Details of the current workflow step in the Editor. | | `editor.workflow_step.id` | **Type:** `integer`**Description:** The numeric identifier of the workflow step. | | `editor.workflow_step.title` | **Type:** `string`**Description:** Title of the workflow step. | | `editor.workflow_step.type` | **Type:** `string`**Description:** Type of the workflow step. | ### [AP.getJwtToken(callback)](#get-jwt-token) [Section titled “AP.getJwtToken(callback)”](#get-jwt-token) The method retrieves a JWT token string. Used to get a current token. If the previous token has a validity period of less than one minute, the method will generate and return a new token. Example: ```js AP.getJwtToken(function(jwtToken) { console.log("String: JWT token", jwtToken); }); ``` Properties: | | | | ---------- | ----------------------------------------------------------------------------------- | | `callback` | **Type:** `function`**Description:** The callback that handles the response string. | ### [AP.getTheme()](#get-theme) [Section titled “AP.getTheme()”](#get-theme) The method retrieves information about the currently selected theme. Example: ```js AP.getTheme(function(theme) { light | dark }); ``` Properties: | | | | ---------- | ----------------------------------------------------------------------------------- | | `callback` | **Type:** `function`**Description:** The callback that handles the response string. | ### [AP.redirect(path)](#redirect) [Section titled “AP.redirect(path)”](#redirect) The method redirects the user to a specified path within the application. Example: ```js AP.redirect('/profile'); ``` Properties: | | | | ------ | ------------------------------------------------------------------------------------------ | | `path` | **Type:** `string`**Description:** The relative path to which the user will be redirected. | ## [Editor Module Actions](#editor-module-actions) [Section titled “Editor Module Actions”](#editor-module-actions) Editor module actions provide a possibility to get information from the Editor UI. These actions are available only on pages that are loaded in an iframe in the Editor-panels module. ### [AP.editor.getString(callback)](#get-string) [Section titled “AP.editor.getString(callback)”](#get-string) The method retrieves information about the active string currently highlighted in the Editor. Example: ```js AP.editor.getString(function(stringData) { console.log("Object: source string", stringData); }); ``` Properties: | | | | ---------- | ----------------------------------------------------------------------------------- | | `callback` | **Type:** `function`**Description:** The callback that handles the response object. | ### [AP.editor.getSelectedStrings(callback)](#get-selected-strings) [Section titled “AP.editor.getSelectedStrings(callback)”](#get-selected-strings) This method retrieves information about the currently selected strings in the Editor, along with their translations for the currently selected language. Example: ```js AP.editor.getSelectedStrings(function(stringData) { console.log("Array: objects with source string and translations for selected languages", stringData); }) ``` Properties: | | | | ---------- | ----------------------------------------------------------------------------------- | | `callback` | **Type:** `function`**Description:** The callback that handles the response object. | ### [AP.editor.getTranslations(callback)](#get-translations) [Section titled “AP.editor.getTranslations(callback)”](#get-translations) The method retrieves a list of translations suggested to the currently selected string in the Editor. Additionally, the response contains information about the translation author, the number of votes, and approval on the current workflow step. Example: ```js AP.editor.getTranslations(function(translationsData) { console.log("Array: translation objects", translationsData); }); ``` Properties: | | | | ---------- | ---------------------------------------------------------------------------------- | | `callback` | **Type:** `function`**Description:** The callback that handles the response array. | ### [AP.editor.getTopTranslation(callback)](#get-top-translation) [Section titled “AP.editor.getTopTranslation(callback)”](#get-top-translation) The method retrieves a top translation (the translation with the highest priority) for the currently selected source string in the Editor. Example: ```js AP.editor.getTopTranslation(function(topTranslationData) { console.log("Object: top translation", topTranslationData); }); ``` Properties: | | | | ---------- | ----------------------------------------------------------------------------------- | | `callback` | **Type:** `function`**Description:** The callback that handles the response object. | ### [AP.editor.setTranslation(text)](#set-translation) [Section titled “AP.editor.setTranslation(text)”](#set-translation) The method sets the translation text for the currently selected source string in the Editor. Example: ```js AP.editor.setTranslation("Hello,"); ``` Properties: | | | | ------ | ------------------------------------------------------------------------------------------------------------------- | | `text` | **Type:** `string`**Description:** The translation text that is being set for the currently selected source string. | ### [AP.editor.appendTranslation(text)](#append-translation) [Section titled “AP.editor.appendTranslation(text)”](#append-translation) The method appends the translation text next to the cursor for the currently selected source string in the Editor. Example: ```js AP.editor.appendTranslation(" {user},"); ``` Properties: | | | | ------ | ------------------------------------------------------------------------------------------------------------------------------------------- | | `text` | **Type:** `string`**Description:** The translation text that is being appended next to the cursor for the currently selected source string. | ### [AP.editor.clearTranslation()](#clear-translation) [Section titled “AP.editor.clearTranslation()”](#clear-translation) The method clears the translation text for the currently selected source string in the Editor. Example: ```js AP.editor.clearTranslation(); ``` ### [AP.editor.setFocus()](#set-focus) [Section titled “AP.editor.setFocus()”](#set-focus) The method sets focus on the translation field in the Editor. Example: ```js AP.editor.setFocus(); ``` ### [AP.editor.noticeMessage(message)](#notice-message) [Section titled “AP.editor.noticeMessage(message)”](#notice-message) The method displays the notice message text in the Editor. Example: ```js AP.editor.noticeMessage("Message text"); ``` Properties: | | | | ------ | ----------------------------------------------------------------------------------- | | `text` | **Type:** `string`**Description:** The notice message text that is being displayed. | ### [AP.editor.successMessage(message)](#success-message) [Section titled “AP.editor.successMessage(message)”](#success-message) The method displays the success message text in the Editor. Example: ```js AP.editor.successMessage("Message text"); ``` Properties: | | | | ------ | ------------------------------------------------------------------------------------ | | `text` | **Type:** `string`**Description:** The success message text that is being displayed. | ### [AP.editor.errorMessage(message)](#error-message) [Section titled “AP.editor.errorMessage(message)”](#error-message) The method displays the error message text in the Editor. Example: ```js AP.editor.errorMessage("Message text"); ``` Properties: | | | | ------ | ---------------------------------------------------------------------------------- | | `text` | **Type:** `string`**Description:** The error message text that is being displayed. | ### [AP.editor.setApplicationNotification(1)](#set-application-notification) [Section titled “AP.editor.setApplicationNotification(1)”](#set-application-notification) The method sets a notification count next to an app icon in the right panel in the Editor. Example: ```js AP.editor.setApplicationNotification(1); ``` Properties: | | | | ------- | ------------------------------------------------------------------ | | `count` | **Type:** `integer`**Description:** The notification count number. | ### [AP.editor.clearApplicationNotification()](#clear-application-notification) [Section titled “AP.editor.clearApplicationNotification()”](#clear-application-notification) The method clears a notification count next to an app icon in the right panel in the Editor. Example: ```js AP.editor.clearApplicationNotification(); ``` ### [AP.editor.getCustomFilter(callback)](#get-custom-filter) [Section titled “AP.editor.getCustomFilter(callback)”](#get-custom-filter) The method retrieves the current Advanced filter applied in the Editor. Example: ```js AP.editor.getCustomFilter(function(customFilter) { console.log(customFilter); }); ``` Properties: | | | | ---------- | ---------------------------------------------------------------------------------------- | | `callback` | **Type:** `function`**Description:** The callback that handles the custom filter object. | ### [AP.editor.setCustomFilter(customFilter)](#set-custom-filter) [Section titled “AP.editor.setCustomFilter(customFilter)”](#set-custom-filter) The method sets a Advanced filter in the Editor. Example: ```js AP.editor.setCustomFilter({ //filter object }); ``` Properties: | | | | -------------- | -------------------------------------------------------------------------- | | `customFilter` | **Type:** `object`**Description:** The custom filter object to be applied. | ### [AP.editor.resetCustomFilter()](#reset-custom-filter) [Section titled “AP.editor.resetCustomFilter()”](#reset-custom-filter) The method resets the Advanced filter to the default state. Example: ```js AP.editor.resetCustomFilter(); ``` ### [AP.editor.getFilter(callback)](#get-filter) [Section titled “AP.editor.getFilter(callback)”](#get-filter) The method retrieves the current filter applied in the Editor. Example: ```js AP.editor.getFilter(function(filter) { console.log(filter); }); ``` Properties: | | | | ---------- | ----------------------------------------------------------------------------- | | `callback` | **Type:** `function`**Description:** The callback that handles the filter ID. | ### [AP.editor.setFilter(filterNumber)](#set-filter) [Section titled “AP.editor.setFilter(filterNumber)”](#set-filter) The method sets a filter in the Editor. Example: ```js AP.editor.setFilter(filterNumber); ``` Properties: | | | | -------------- | ---------------------------------------------------------------- | | `filterNumber` | **Type:** `integer`**Description:** The filter ID to be applied. | **Filter Option IDs:** * All, Untranslated First – 0 * Untranslated – 2 * Show All – 3 * Approved – 4 * Translated, Not Approved – 5 * Hidden – 6 * With Comments – 7 * Unsaved translations – 39 * Translated by TM – 30 * Translated by MT – 31 * Translated by TM or MT – 10 * Advanced Filter – 12 * With unresolved issues (All languages) – 13 * Need to be voted – 15 * Without comments – 16 * With unresolved issues (Current language) – 17 * QA Issues (All) – 18 * QA Issues (Empty translation) – 19 * QA Issues (Variables mismatch) – 20 * QA Issues (Tags mismatch) – 21 * QA Issues (Punctuation mismatch) – 22 * QA Issues (Character case mismatch) – 23 * QA Issues (Spaces mismatch) – 24 * QA Issues (Length issues) – 25 * QA Issues (Special character mismatch) – 26 * QA Issues (“Incorrect translation” issues) – 27 * QA Issues (Spelling) – 28 * QA Issues (ICU syntax) – 29 * QA Issues (Consistent terminology) – 32 * QA Issues (Without QA issues) – 33 * QA Issues (Duplicate translation) – 35 * QA Issues (FTL syntax) – 36 * QA Issues (Android syntax) – 37 Caution The following list may not include some custom filter options added using the Crowdin Apps. To get the full list of available filter options, use the [`AP.editor.getFiltersList(filters)`](#get-filters-list) method. ### [AP.editor.getFiltersList(filters)](#get-filters-list) [Section titled “AP.editor.getFiltersList(filters)”](#get-filters-list) The method retrieves the list of available filter options in the Editor. Example: ```js AP.editor.getFiltersList(function(filters) { console.log(filters); }); ``` Properties: | | | | ---------- | ---------------------------------------------------------------------------------- | | `callback` | **Type:** `function`**Description:** The callback that handles the filters object. | ### [AP.editor.getPage(callback)](#get-page) [Section titled “AP.editor.getPage(callback)”](#get-page) The method retrieves the current page number in the Editor. Example: ```js AP.editor.getPage(function(page) { console.log(page); }); ``` Properties: | | | | ---------- | ------------------------------------------------------------------------------- | | `callback` | **Type:** `function`**Description:** The callback that handles the page number. | ### [AP.editor.setPage(pageNumber)](#set-page) [Section titled “AP.editor.setPage(pageNumber)”](#set-page) The method sets a page number in the Editor. Example: ```js AP.editor.setPage(pageNumber); ``` Properties: | | | | ------------ | -------------------------------------------------------------- | | `pageNumber` | **Type:** `integer`**Description:** The page number to be set. | ### [AP.editor.getProjectTargetLanguages(callback)](#get-project-target-languages) [Section titled “AP.editor.getProjectTargetLanguages(callback)”](#get-project-target-languages) The method retrieves the target languages for the current project. Example: ```js AP.editor.getProjectTargetLanguages(function(languages) { console.log(languages); }); ``` Properties: | | | | ---------- | ------------------------------------------------------------------------------------------ | | `callback` | **Type:** `function`**Description:** The callback that handles the target languages array. | ### [AP.editor.setTargetLanguage(languageIdOrIds, callback)](#set-target-language) [Section titled “AP.editor.setTargetLanguage(languageIdOrIds, callback)”](#set-target-language) The method sets the target language(s) in the Editor. Example: ```js AP.editor.setTargetLanguage(languageId | languageIds, function(res) { console.log(res)// response message }); ``` Properties: | | | | ----------------- | ----------------------------------------------------------------------------------------------------- | | `languageIdOrIds` | **Type:** `string \| array`**Description:** The ID or array of IDs of the target languages to be set. | | `callback` | **Type:** `function`**Description:** The callback that handles the response. | ### [AP.editor.getCroqlFilter(callback)](#get-croql-filter) [Section titled “AP.editor.getCroqlFilter(callback)”](#get-croql-filter) The method retrieves the current CroQL filter applied in the Editor. Example: ```js AP.editor.getCroqlFilter(function(croql) { console.log(croql); }); ``` Properties: | | | | ---------- | ------------------------------------------------------------------------- | | `callback` | **Type:** `function`**Description:** The callback that handles the CroQL. | ### [AP.editor.setCroqlFilter(croql)](#set-croql-filter) [Section titled “AP.editor.setCroqlFilter(croql)”](#set-croql-filter) The method sets a CroQL filter in the Editor. Example: ```js AP.editor.setCroqlFilter(croql); ``` Properties: | | | | ------- | ------------------------------------------------------------------ | | `croql` | **Type:** `string`**Description:** The CroQL filter to be applied. | ### [AP.editor.resetCroqlFilter()](#reset-croql-filter) [Section titled “AP.editor.resetCroqlFilter()”](#reset-croql-filter) The method resets the CroQL filter to the default state. Example: ```js AP.editor.resetCroqlFilter(); ``` ### [AP.editor.search(text, options)](#search) [Section titled “AP.editor.search(text, options)”](#search) The method performs a search within the Editor. Example: ```js AP.editor.search('text', { searchStrict: false, searchFullMatch: false, caseSensitive: false, search_option: 0 // 1 - Strings, 2 - Context, 3 - Translations, 4 - Identifier (Key), 0 - Everything }); ``` Properties: | | | | --------- | ------------------------------------------------------ | | `text` | **Type:** `string`**Description:** The search text. | | `options` | **Type:** `object`**Description:** The search options. | ### [AP.editor.setWorkflowStepStatusFilter(status)](#set-workflow-step-status-filter) [Section titled “AP.editor.setWorkflowStepStatusFilter(status)”](#set-workflow-step-status-filter) The method sets a workflow step status filter in the Editor (Crowdin Enterprise only). Example: ```js AP.editor.setWorkflowStepStatusFilter('ALL' | 'TODO' | 'DONE' | 'INCOMPLETE'); ``` Properties: | | | | -------- | ---------------------------------------------------------------------- | | `status` | **Type:** `string`**Description:** The workflow step status to be set. | ### [AP.editor.getWorkflowStepStatusFilter(callback)](#get-workflow-step-status-filter) [Section titled “AP.editor.getWorkflowStepStatusFilter(callback)”](#get-workflow-step-status-filter) The method retrieves the current workflow step status filter applied in the Editor. Example: ```js AP.editor.getWorkflowStepStatusFilter(function(step) { console.log(step); }); ``` Properties: | | | | ---------- | ----------------------------------------------------------------------------------------------- | | `callback` | **Type:** `function`**Description:** The callback that handles the workflow step status filter. | ### [AP.editor.getMode(callback)](#get-mode) [Section titled “AP.editor.getMode(callback)”](#get-mode) The method retrieves the current mode of the Editor. Example: ```js AP.editor.getMode(function(mode) { console.log(mode); }); ``` Properties: | | | | ---------- | -------------------------------------------------------------------------------- | | `callback` | **Type:** `function`**Description:** The callback that handles the current mode. | ### [AP.editor.setMode(mode)](#set-mode) [Section titled “AP.editor.setMode(mode)”](#set-mode) The method sets a mode in the Editor. Example: ```js AP.editor.setMode('translate' | 'proofread' | 'review' | 'multilingual'); ``` Properties: | | | | ------ | ------------------------------------------------------ | | `mode` | **Type:** `string`**Description:** The mode to be set. | ## [Event Module Actions](#event-module-actions) [Section titled “Event Module Actions”](#event-module-actions) Event module actions provide a possibility to work with events, enable the Crowdin app to respond to events that happen in Crowdin UI, and trigger its own events. Read more about [supported events](#supported-events). ### [AP.events.once(event, callback)](#events-once) [Section titled “AP.events.once(event, callback)”](#events-once) The method subscribes a one-time listener to detect events with a specified name. The listener is unsubscribed as soon as the first event is processed. Example: ```js AP.events.once("event", function(eventData) { console.log("Event data", eventData); }); ``` Properties: | | | | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `event` | **Type:** `string`**Description:** The name of the event the callback of the function should react to. | | `callback` | **Type:** `function`**Description:** The callback that handles the event. Depending on the event’s type, there could be either one or a few arguments in the callback. | ### [AP.events.on(event, callback)](#events-on) [Section titled “AP.events.on(event, callback)”](#events-on) The method subscribes a listener to all events with a specified name. Example: ```js AP.events.on("event", function(eventData) { console.log("Event data", eventData); }); ``` Properties: | | | | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `event` | **Type:** `string`**Description:** The name of the event the callback of the function should react to. | | `callback` | **Type:** `function`**Description:** The callback that handles the event. Depending on the event’s type, there could be either one or a few arguments in the callback. | ### [AP.events.off(event, callback)](#events-off) [Section titled “AP.events.off(event, callback)”](#events-off) The method unsubscribes a listener for an event with a specified name. Example: ```js function callback(eventData) { console.log("Event data", eventData); }; AP.events.on("event", callback); AP.events.off("event", callback); ``` Properties: | | | | ---------- | ------------------------------------------------------------------------------------------------------------------ | | `event` | **Type:** `string`**Description:** The name of the event the callback of the function should be unsubscribed from. | | `callback` | **Type:** `function`**Description:** The callback that was previously subscribed to the event. | ### [AP.events.ofAll(event)](#events-off-all) [Section titled “AP.events.ofAll(event)”](#events-off-all) The method unsubscribes all previously subscribed listeners from the specified event. Example: ```js AP.events.offAll("event"); ``` | | | | ------- | --------------------------------------------------------------------------------------------------- | | `event` | **Type:** `string`**Description:** The name of the event all listeners should be unsubscribed from. | ### [AP.events.onAny(callback)](#events-on-any) [Section titled “AP.events.onAny(callback)”](#events-on-any) The method subscribes to all events from Crowdin UI. Example: ```js AP.events.onAny(function(event, data) { console.log("Event name string", event); console.log("Event data", data); }); ``` Properties: | | | | ---------- | -------------------------------------------------------------------------------------------------------------------------------- | | `callback` | **Type:** `function`**Description:** The callback that receives the name of the event and all the data transferred to the event. | ### [AP.events.offAny(callback)](#events-off-any) [Section titled “AP.events.offAny(callback)”](#events-off-any) The method unsubscribes a listener from all events. Example: ```js function callback(event, data) { console.log("Event name string", event); console.log("Event data", data); }; AP.events.onAny(callback); AP.events.offAny(callback); ``` Properties: | | | | ---------- | ------------------------------------------------------------------------------------ | | `callback` | **Type:** `function`**Description:** The callback that was subscribed to all events. | ### [Supported Events](#supported-events) [Section titled “Supported Events”](#supported-events) Check out the list of supported events that could be passed to the Crowdin Apps JS library in the following table: | Event | Details | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `string.change` | The event emitted when a user switches from one string to another.```json { "id": 3939912, "text": "source string", "context": "string context", "max_length": 35, "file": { "id": 26588, "name": "file name.csv" } } ``` | | `string.selected` | The event emitted when a user selects strings.```json { "string": { "id": 3939912, "identifier": "", "text": "source string", "context": "string context", "max_length": 0, "file": { "id": 26588, "name": "file name.csv" } }, "translations": { "fr": [ { "id": 14861530, "string_id": 3939912, "text": "chaîne source", "target_language_id": "fr", "approved": false, "author": { "id": "1", "login": "user.login", "name": "User Name", "avatar_url": "https://avatar.com/avatar.png" }, "created_at": "2025-05-05T05:05:34-04:00" } ] } } ``` | | `textarea.edited` | The event emitted when a user performs any changes in the translation field.```json { "id": 3939912, "text": "source string", "context": "string context", "max_length": 0, "file": { "id": 26588, "name": "file name.csv" }, "oldText": "chaîne de caractères source", "newText": "chaîne source" } ``` | | `translation.added` | The event emitted when a user saves a translation for the current string.```json { "id": 422648, "string_id": 3939912, "text": "chaîne source", "target_language_id": "fr", "votes_rating": 0, "approved": false, "author": { "id": "1", "login": "user.login", "name": "User Name", "avatar_url": "https://avatar.com/avatar.png" }, "created_at": "2022-12-09T12:17:37-05:00" } ``` | | `translation.deleted` | The event emitted when a user deletes a translation.```json { "id": 422560, "string_id": 3939912 } ``` | | `translation.restored` | The event emitted when a user restores a deleted translation.```json { "id": 422648, "string_id": 3939912, "text": "chaîne source", "target_language_id": "fr", "“votes_rating”": 0, "author": { "id": "1", "login": "user.login", "name": "User Name", "avatar_url": "https://avatar.com/avatar.png" }, "created_at": "2022-12-09T12:17:37-05:00" } ``` | | `translation.vote` | The event emitted when a user votes for a translation.```json { "id": 422648, "string_id": 3939912, "text": "chaîne source", "target_language_id": "fr", "votes_rating": 1, "approved": false, "author": { "id": "1", "login": "user.login", "name": "User Name", "avatar_url": "https://avatar.com/avatar.png" }, "created_at": "2022-12-09T12:17:37-05:00" } ``` | | `translation.approve` | The event emitted when a user approves a translation.```json { "id": 422648, "string_id": 3939912, "text": "chaîne source", "target_language_id": "fr", "votes_rating": 0, "approved": true, "author": { "id": "1", "login": "user.login", "name": "User Name", "avatar_url": "https://avatar.com/avatar.png" }, "created_at": "2022-12-09T12:17:37-05:00", "approver": { "id": "1", "login": "super.user", "name": "Super User", "avatar_url": "https://avatar.com/avatar.png" }, "approved_at": "2022-12-09T17:20:28.655Z" } ``` | | `translation.disapprove` | The event emitted when a user removes a translation approval.```json { "id": 422406, "string_id": 3939858, "text": "chaîne de caractères source", "target_language_id": "fr", "votes_rating": 0, "approved": false, "author": { "id": "1", "login": "user.login", "name": "User Name", "avatar_url": "https://avatar.com/avatar.png" }, "created_at": "2022-12-08T12:29:07-05:00" } ``` | | `language.change` | The event emitted when a user changes a target language in the Editor.```json { "project_id": "15", "organization_id": "200000000", "editor": { "mode": "translate", "theme": "dark;", "source_language_id": "en", "target_language_id": "fr", "file": 26588, "workflow_step": { "id": 77, "title": "Translation", "type": "Translate" } } } ``` | | `file.change` | The event emitted when a user changes a file in the Editor.```json { "project_id": "15", "organization_id": "200000000", "editor": { "mode": "translate", "theme": "dark;", "source_language_id": "en", "target_language_id": "fr", "file": 26574, "workflow_step": { "id": 77, "title": "Translation", "type": "Translate" } } } ``` | | `theme.changed` | The event emitted when a user switches from one theme to another.```json "light" ``` | # AI Prompt Provider Module > Create external Pre-translate and Assist prompts The AI Prompt Provider module is designed to streamline the process of generating prompts for both custom and natively supported AI providers. By integrating this module into your app, you can create external Pre-translate and Assist prompts that leverage various types of contextual information from your Crowdin projects. The module allows you to compile prompts externally, ensuring they are ready for execution by the AI provider without any further modifications. This approach is particularly beneficial for users with fine-tuned models that require specific prompt structures that differ from those generated by Crowdin, allowing for seamless integration and optimal performance. ## [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-prompt-provider": [ { "key": "ai-prompt-provider", "name": "AI prompt provider", "logo": "/logo.png", "configuratorUrl": "/configurator", "compileUrl": "/compile", "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. | | `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. | | `configuratorUrl` | **Type:** `string`**Required:** no**Description:** The relative URL for an iframe that provides a set of options stored in the prompt and will be sent during the compilation. | | `compileUrl` | **Type:** `string`**Required:** yes**Description:** The relative URL to the API endpoint used for prompt compilation. | | `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 AI Prompt Provider App and Crowdin](#communication-between-ai-prompt-provider-app-and-crowdin) [Section titled “Communication between AI Prompt Provider App and Crowdin”](#communication-between-ai-prompt-provider-app-and-crowdin) The system collects the strings requiring translation along with all related context and sends this data to the AI Prompt Provider app. The app processes the data, and using `compileUrl`, compiles a prompt optimized for a specific AI model and requirements. The compiled prompt is then returned back to Crowdin and sent to the chosen AI provider. ## [Request to the App from Crowdin for Assist Action](#request-to-the-app-from-crowdin-for-assist-action) [Section titled “Request to the App from Crowdin for Assist Action”](#request-to-the-app-from-crowdin-for-assist-action) Request payload example: ```json { "action": "assist", "provider": "open_ai", "model": "gpt-4o", "limitation": { "contextWindow": 128000, "output": 4096 }, "options": null, "payload": { "project": { "id": 748586, "name": "Umbrella Android", "description": "Localization project for Umbrella Android" }, "sourceLanguage": { "id": "en", "name": "English", }, "targetLanguage": { "id": "de", "name": "German", }, "strings": [ { "id": 5511717, "text": "Welcome!", "key": "welcome", "context": "welcome", "maxLength": null, "pluralForm": null } ], "siblingsStrings": { "previous": null, "next": { "id": 5511718, "text": "Save as...", "key": "save_as", "context": "save_as", "maxLength": null } }, "filteredStrings": [ { "id": 5511717, "text": "Welcome!", "key": "welcome", "context": "welcome", "maxLength": null }, { "id": 5511718, "text": "Save as...", "key": "save_as", "context": "save_as", "maxLength": null }, { "id": 5511719, "text": "View", "key": "view", "context": "view", "maxLength": null } ], "tmSuggestions": [ { "tmId": 736613, "phraseId": 316642, "source": { "text": "Welcome!" }, "target": { "text": "Willkommen!" }, "relevant": 100, "updatedAt": "2024-05-17T09:09:32+00:00" } ], "terms": [ { "glossaryId": 728017, "conceptId": 211993, "source": { "id": 357901, "userId": 13575027, "languageId": "en", "text": "Welcome", "description": null, "partOfSpeech": "PROPN", "status": null, "type": null, "gender": null, "note": null, "url": null, "lemma": "welcome", "createdAt": "2024-06-21T07:44:26+00:00", "updatedAt": "2024-06-21T07:44:26+00:00" }, "target": { "id": 357902, "userId": 13575027, "languageId": "de", "text": "Willkommen", "description": null, "partOfSpeech": "PROPN", "status": null, "type": null, "gender": null, "note": null, "url": null, "lemma": "willkommen", "createdAt": "2024-06-21T07:44:26+00:00", "updatedAt": "2024-06-21T07:44:26+00:00" } } ], "file": { "id": 4549, "name": "crowdin_sample_android.xml", "title": null, "context": "File context", "type": "android", "path": "/crowdin_sample_android.xml" } } } ``` ## [Request to the App from Crowdin for Pre-translate Action](#request-to-the-app-from-crowdin-for-pre-translate-action) [Section titled “Request to the App from Crowdin for Pre-translate Action”](#request-to-the-app-from-crowdin-for-pre-translate-action) Request payload example: ```json { "action": "pre_translate", "provider": "open_ai", "model": "gpt-4o", "limitation": { "contextWindow": 128000, "output": 4096 }, "options": null, "payload": { "project": { "id": 748586, "name": "Umbrella Android", "description": "Localization project for Umbrella Android" }, "sourceLanguage": { "id": "en", "name": "English", }, "targetLanguage": { "id": "de", "name": "German", }, "strings": [ { "id": 5511717, "text": "Welcome!", "key": "welcome", "context": "welcome", "maxLength": null, "pluralForm": null }, { "id": 5511718, "text": "Save as...", "key": "save_as", "context": "save_as", "maxLength": null, "pluralForm": null }, { "id": 5511719, "text": "View", "key": "view", "context": "view", "maxLength": null, "pluralForm": null } ], "tmSuggestions": [ { "tmId": 736613, "phraseId": 316642, "source": { "text": "Welcome!" }, "target": { "text": "Willkommen!" }, "relevant": 100, "updatedAt": "2024-05-17T09:09:32+00:00" }, { "tmId": 736613, "phraseId": 316643, "source": { "text": "Save as..." }, "target": { "text": "Speichern unter..." }, "relevant": 100, "updatedAt": "2024-05-17T09:09:59+00:00" } ], "terms": [ { "glossaryId": 728017, "conceptId": 211993, "source": { "id": 357901, "userId": 13575027, "languageId": "en", "text": "Welcome", "description": null, "partOfSpeech": "PROPN", "status": null, "type": null, "gender": null, "note": null, "url": null, "lemma": "welcome", "createdAt": "2024-06-21T07:44:26+00:00", "updatedAt": "2024-06-21T07:44:26+00:00" }, "target": { "id": 357902, "userId": 13575027, "languageId": "de", "text": "Willkommen", "description": null, "partOfSpeech": "PROPN", "status": null, "type": null, "gender": null, "note": null, "url": null, "lemma": "willkommen", "createdAt": "2024-06-21T07:44:26+00:00", "updatedAt": "2024-06-21T07:44:26+00:00" } }, { "glossaryId": 728017, "conceptId": 211992, "source": { "id": 357899, "userId": 13575027, "languageId": "en", "text": "Save", "description": null, "partOfSpeech": "PROPN", "status": "PREFERRED", "type": "FULL_FORM", "gender": "OTHER", "note": null, "url": null, "lemma": "save", "createdAt": "2024-05-17T09:12:36+00:00", "updatedAt": "2024-05-17T09:12:36+00:00" }, "target": null } ], "file": { "id": 4549, "name": "crowdin_sample_android.xml", "title": null, "context": "File context", "type": "android", "path": "/crowdin_sample_android.xml", "contentUrl": "https://crowdin-importer.s3.eu-central-1.amazonaws.com/72057531/748586/3407.xml?response-content-disposition=attachment%3B%20filename%3D%22crowdin_sample_android.xml%22&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={aws-access-key-id}%2F20240621%2Feu-central-1%2Fs3%2Faws4_request&X-Amz-Date=20240621T080230Z&X-Amz-SignedHeaders=host&X-Amz-Expires=900&X-Amz-Signature={signature}" } } } ``` ## [Request to the App from Crowdin for Alignment Action](#request-to-the-app-from-crowdin-for-alignment-action) [Section titled “Request to the App from Crowdin for Alignment Action”](#request-to-the-app-from-crowdin-for-alignment-action) Request payload example: ```json { "action": "alignment", "provider": "open_ai", "model": "gpt-4o", "limitation": { "contextWindow": 128000, "output": 4096 }, "options": null, "payload": { "project": { "id": 748586, "name": "Umbrella Android", "description": "Localization project for Umbrella Android" }, "sourceLanguage": { "id": "en", "name": "English", }, "targetLanguage": { "id": "de", "name": "German", }, "alignmentPairs": [ { "id": "0c92b4f9-6475-4c9e-9fe2-efc76f2917d5", "source": "Your password has been reset successfully!", "translation": "Ihr Passwort wurde erfolgreich zurückgesetzt!" }, { "id": "9e5fa3ab-3629-49ef-868d-09a6ea5f3df9", "source": "Are you sure you want to delete this message?", "translation": "Sind Sie sicher, dass Sie diese Nachricht löschen möchten?" } ] } } ``` ## [Expected Response from the App (Plain content)](#expected-response-from-the-app-plain-content) [Section titled “Expected Response from the App (Plain content)”](#expected-response-from-the-app-plain-content) Response payload example: ```json { "data": { "content": "" } } ``` ## [Expected Response from the App (Content parts)](#expected-response-from-the-app-content-parts) [Section titled “Expected Response from the App (Content parts)”](#expected-response-from-the-app-content-parts) Response payload example: ```json { "data": { "content": [ { "type": "text", "text": "" }, { "type": "image", "mimeType": "", "url": "" } ] } } ``` The structure of the responses from the app should correspond to the presented examples, otherwise Crowdin will consider them invalid. # AI Request Processor Modules > Use AI Request Processor Modules to customize AI request and response flows at different stages of processing AI Request Processor Modules allow you to customize the way Crowdin handles AI-related requests and responses at different stages of processing. These modules give you full control over how data is prepared before it’s sent to an AI provider, and how the response is handled once it’s returned. Crowdin supports four types of processors that can be used independently or combined within an app: * **Pre-compile** – executed before the prompt is compiled. Used to modify raw data such as source strings, TM matches, or glossary terms. * **Post-compile** – executed after the prompt is compiled. Used to adjust request body or headers before it’s sent to the AI provider. * **Pre-parse** – executed after receiving the AI provider’s raw response. Used to modify the response body before parsing. * **Post-parse** – executed after the response has been parsed into structured Crowdin data. Used to adjust the final result based on the action, such as pre-translation or QA checks. These modules are designed to be backend-only and do not include a user interface. However, you can add UI functionality to your app using other modules (e.g., Project Tool, Organization Menu). ## [Access](#access) [Section titled “Access”](#access) You can grant access to these modules to the following user categories: For Crowdin: * All project members For Crowdin Enterprise: * All users in the organization projects ## [Structure](#structure) [Section titled “Structure”](#structure) manifest.json ```json { "modules": { "ai-request-pre-compile": [ { "key": "liquid-templates-everywhere-ai-request-pre-compile", "processorUrl": "/ai-request-processor/ai-request-pre-compile" } ], "ai-request-post-compile": [ { "key": "liquid-templates-everywhere-ai-request-post-compile", "processorUrl": "/ai-request-processor/ai-request-post-compile" } ], "ai-request-pre-parse": [ { "key": "liquid-templates-everywhere-ai-request-pre-parse", "processorUrl": "/ai-request-processor/ai-request-pre-parse" } ], "ai-request-post-parse": [ { "key": "liquid-templates-everywhere-ai-request-post-parse", "processorUrl": "/ai-request-processor/ai-request-post-parse" } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `processorUrl` | **Type:** `string`**Required:** yes**Description:** Relative path to the endpoint in your app that will handle incoming requests for this processor. | ## [How It Works](#how-it-works) [Section titled “How It Works”](#how-it-works) Each AI Request Processor Module runs at a specific stage of the AI request lifecycle in Crowdin. These modules allow you to modify the data being sent to or received from an AI provider. They are backend-only and receive data via POST requests from Crowdin. Each request includes two parts: * `requestData` – the data object that can be modified and returned. Its structure depends on the processor type and the specific AI action. For example, it may contain source strings, prompt body and headers, or parsed results. * `requestContext` – a read-only object that provides metadata about the request. This includes the AI provider, model, limitations, project ID, and the prompt action being performed. You can use this to adjust your processing logic based on context. Crowdin will send the request to the `processorUrl` defined for the corresponding module. Your app must return the modified `requestData` in the same structure as received. Only update the parts that require transformation based on your logic. The following sections describe how each module type works and when it is triggered. ### [Pre-Compile Processor](#pre-compile-processor) [Section titled “Pre-Compile Processor”](#pre-compile-processor) Runs **before** the prompt is compiled. * **Receives:** raw data used to build the prompt — such as strings, project name, glossary terms, and TM matches. * **Returns:** modified version of the input data. * **Use cases:** * Inject additional placeholders before compilation. * Filter or clean strings before building a prompt (e.g., remove unwanted patterns or binary data). * Customize prompt input based on project metadata. ### [Post-Compile Processor](#post-compile-processor) [Section titled “Post-Compile Processor”](#post-compile-processor) Runs **after** the prompt is compiled but **before** it is sent to the AI provider. * **Receives:** full request details including `action`, `provider`, `model`, `limitation`, and compiled `payload`. The payload contains `body` and `headers`. * **Returns:** modified version of the payload (except for sensitive data like API keys, which are not included). * **Use cases:** * Adjust the request format for specific providers. * Inject tool definitions or metadata. * Route requests through a proxy or log outgoing content. ### [Pre-Parse Processor](#pre-parse-processor) [Section titled “Pre-Parse Processor”](#pre-parse-processor) Runs **after** receiving a response from the AI provider but **before** parsing. * **Receives:** `action` and the raw `body` from the provider’s response. * **Returns:** modified body for further parsing. * **Use cases:** * Clean or normalize the AI response before parsing. * Handle response transformations like extracting JSON from markdown blocks. * Remove incomplete or invalid chunks from streamed content. ### [Post-Parse Processor](#post-parse-processor) [Section titled “Post-Parse Processor”](#post-parse-processor) Runs **after** the response is parsed into structured Crowdin data. * **Receives:** `action` and the parsed `payload`, which varies by context. * **Returns:** modified result payload. * **Use cases:** * Apply final adjustments to AI-generated strings. * Filter out unwanted suggestions based on custom rules. * Post-process QA results or translation units before saving. # AI Tools Module > Extend the Crowdin Editor’s AI Assistant with callable tools that trigger secure logic and return results for contextual use. This module helps you extend the AI Assistant in the Crowdin Editor with custom tools that provide access to functionality not available by default. Once you create this kind of app, you’ll be able to define callable functions using the function calling approach supported by modern AI models. For example, you can generate project-specific reports, fetch internal data, or trigger secure API calls. The results returned by your tools can then be processed and interpreted by the AI Assistant based on user prompts. For rendering UI elements in the Crowdin Editor, see the [AI Tools Widget Module](/developer/crowdin-apps-module-ai-tools-widget/). ## [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-tools": [ { "key": "custom-generate-report", "toolType": "function", "function": { "name": "generate_project_report", "description": "Generate a custom project report and return raw data for AI summarization.", "parameters": { "type": "object", "properties": { "projectId": { "type": "string", "description": "The ID of the project to generate a report for." }, "reportType": { "type": "string", "description": "The type of report to generate (e.g., 'activity', 'costs')." } }, "required": [ "projectId", "reportType" ] } }, "url": "/ai-tools/generate_report" } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `toolType` | **Type:** `string`**Required:** yes**Allowed value:** `function`**Description:** Type of the tool used in the module. | | `function.name` | **Type:** `string`**Required:** yes**Description:** The human-readable name of the function. | | `function.description` | **Type:** `string`**Required:** yes**Description:** The description of what the function does. Helps AI understand how and when to use it. | | `function.parameters` | **Type:** `object`**Required:** no**Description:** The list of input parameters used by the function. Follows JSON Schema structure. | | `url` | **Type:** `string`**Required:** yes**Description:** The relative URL to the endpoint that implements the function logic. | ## [Example Request and Response](#example-request-and-response) [Section titled “Example Request and Response”](#example-request-and-response) When a tool from the AI Tools module is called by the AI Assistant, Crowdin sends a POST request to the defined `url`. The request includes project and organization context along with the function name and arguments. ### [Request](#request) [Section titled “Request”](#request) ```json { "function": { "name": "generate_project_report", "arguments": "{\"projectId\":\"751531\",\"reportType\":\"translation activity\"}" }, "organization": { "id": 12345678, "baseUrl": "https://{organization-name}.crowdin.com​", "apiBaseUrl": "https://{organization-name}.api.crowdin.com​" }, "project": { "id": 123456, "identifier": "sample-project", "name": "Sample Project", "sourceLanguageId": "en", "targetLanguageIds": ["de", "uk"] } } ``` ### [Response](#response) [Section titled “Response”](#response) ```json { "data": { "content": "{\"total_translations\":4512,\"top_contributor\":\"user_123\",\"term_consistency\":98}" } } ``` The `content` value will be passed to the AI Assistant, which will then process and present it based on the prompt or chat context. # AI Tools Widget Module > Extend the Crowdin Editor’s AI Assistant with custom widgets that render interactive visualizations or interfaces. The AI Tools Widget module allows you to enhance the Crowdin Editor’s AI Assistant by integrating custom data visualizations. By creating this type of app, you can provide interactive graphical representations of project data, such as translation progress, contributor activity, quality metrics, etc. These visualizations are rendered directly in the Editor and enable the AI Assistant to present complex information in a more accessible and insightful manner, helping users make informed decisions. ## [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-tools-widget": [ { "key": "custom-data-visualization", "toolType": "function", "function": { "name": "display_custom_graph", "description": "Render a custom graph based on project-specific data.", "parameters": { "type": "object", "properties": { "dataType": { "type": "string", "description": "The type of data to visualize (e.g., 'translation progress', 'quality assurance')." }, "timeFrame": { "type": "string", "description": "The time frame for the data (e.g., 'last_week', 'last_month')." } }, "required": ["dataType", "timeFrame"] } }, "url": "/ai-tools-widget/display_custom_graph/index.html" } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `toolType` | **Type:** `string`**Required:** yes**Allowed value:** `function`**Description:** Type of the tool used in the module. | | `function.name` | **Type:** `string`**Required:** yes**Description:** The human-readable name of the function. | | `function.description` | **Type:** `string`**Required:** yes**Description:** The description of what the function does. Helps AI understand how and when to use it. | | `function.parameters` | **Type:** `object`**Required:** no**Description:** The list of input parameters used by the function. Follows JSON Schema structure. | | `url` | **Type:** `string`**Required:** yes**Description:** The relative URL to the widget HTML file that will be rendered in the Crowdin Editor UI. The `url` should point to a static HTML file bundled with your Crowdin App that contains the widget’s UI and logic. | ## [Behavior](#behavior) [Section titled “Behavior”](#behavior) Unlike the [AI Tools](/developer/crowdin-apps-module-ai-tools/) module, which receives a structured payload from the AI Assistant as a POST request, the AI Tools Widget module defines a function that renders a custom widget in the Crowdin Editor. This widget is loaded directly in the browser and can interact with the Crowdin UI or call APIs as needed. No server-side request is sent to the defined `url`. Instead, the widget runs entirely in the browser and relies on the HTML and JavaScript logic implemented at that path. The widget is fully responsible for handling its input, data fetching, rendering, and user interactions. # API Module > Communicate with Crowdin App via the API This module allows you to communicate with an app via the API. Once you implement the API module along with the API methods in your app, you’ll be able to access the app’s API methods via the Crowdin API to manage (i.e., modify or delete) the necessary app data. ## [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": { "api": [ { "key": "your-api-module-key", "name": "API method name", "url": "/path", "method": "GET", "description": "Description of the API method", "documentationUrl": "/path/to/documentation" } ] } } ``` ## [Properties](#properties) [Section titled “Properties”](#properties) | | | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------- | | `key` | **Type:** `string`**Required:** yes**Description:** Module identifier within the Crowdin app. | | `name` | **Type:** `string`**Required:** yes**Description:** API method name. | | `url` | **Type:** `string`**Required:** yes**Description:** The relative URL to the API method. | | `method` | **Type:** `string`**Allowed values:** `GET`, `POST`, `PUT`, `PATCH`, `DELETE`**Required:** yes**Description:** API method type. | | `description` | **Type:** `string`**Description:** Description of the API method. | | `documentationUrl` | **Type:** `string`**Description:** The relative URL to the API method documentation. | # 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. ![Custom MT Module](/_astro/module_mt.BF5K9mo0_1VFpB.webp) ## [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 } ] } } ``` ## [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. | | `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. | ## [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 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. ![Editor Right Panel Module](/_astro/module_editor.DQyccU4a_Z2S2oe.webp) ## [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. ![Editor Translations Panel Module](/_astro/module_editor_translations_panel.BqZvLCnx_Z23fKGa.webp) ## [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. ![Organization Menu Module](/_astro/module_org_menu.BagBwdj2_ZspA0T.webp) ## [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. ![Organization Settings Menu Module](/_astro/module_org_settings_menu.C1PINvOB_Kjv8j.webp) ## [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 an additional panel in the project owner's resources The module allows the creation of an additional panel in the project owner’s resources. ![Resources Module](/_astro/module_resources.BgxE7-Gk_ZUX62D.webp) ## [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": { "profile-resources-menu": [ { "key": "your-module-key", "name": "Module name", "url": "/resource-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 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. | # 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. ![Profile Settings Menu Module](/_astro/module_profile_settings.By8SZb6q_xLQa3.webp) ## [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. ![Integrations Module](/_astro/module_integrations.Ck6D68uS_St9rz.webp) ## [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. ![Project Menu Module](/_astro/module_project_menu.Cs8yDAZd_Z1pi3ux.webp) ## [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. ![Tools Module](/_astro/module_tools.DsPLdQLu_Z2ldvkq.webp) ## [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. | # 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); ``` # 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 | ✔ | ✔ | ## [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 Panel | `editor-right-panel` | Project | ✔ | ✔ | | Organization Menu | `organization-menu` | Organization | | ✔ | | Organization Menu (Crowdsource View) | `organization-menu-crowdsource` | Organization | | ✔ | | Resources Menu | `profile-resources-menu` | Account | ✔ | | | Custom MT Engine | `custom-mt` | Account/Organization | ✔ | ✔ | | Context Menu | `context-menu` | Configurable | ✔ | ✔ | | Modal | `modal` | Configurable | ✔ | ✔ | | Custom Spellchecker | `custom-spellchecker` | 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. | # 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 Checkout page](/_astro/crowdin_apps_monetization.jsHVrKXN_Z7TSy5.webp) * Crowdin Enterprise Checkout page ![Crowdin Enterprise Checkout page](/_astro/crowdin_apps_monetization_enterprise.CPHD2akh_Z1ICblv.webp) 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 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. ![A bird.](https://raw.githubusercontent.com/crowdin/crowdin-cli/main/website/static/img/cli-demo-image.png) 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) [![Android SDK](/images/repo-card/mobile-sdk-android.png)](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) [![iOS SDK](/images/repo-card/mobile-sdk-ios.png)](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) [![Flutter SDK](/images/repo-card/flutter-sdk.png)](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) [![OTA JS Client](/images/repo-card/ota-client-js.png)](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) 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. ![In-Context View](/_astro/in_context_strings.DDMkRL1p_Z23Qtx8.webp) 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 ![In-Context Tab in Crowdin](/_astro/in_context_tab.C4CMkfKL_Z11YYDT.webp) * Crowdin Enterprise ![In-Context Tab in Enterprise](/_astro/in_context_tab_e.DAM1DckO_N7mH8.webp) After the integration is successfully set up and you have refreshed your application, you should see the invitation dialog and Crowdin login box. ![Login Window](/_astro/in_context_login.D_4Hzopc_ZlRMIW.webp) ### [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. ![Close Login Window](/_astro/in_context_login_close.COrh_PSo_1hID8s.webp) ### [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**. ![Add Screenshot](/_astro/in_context_add_screenshot.D7C1DMBA_is9iq.webp) 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**. ![Replace Screenshot](/_astro/in_context_replace_screenshot.C9iPP6OH_Z1wbLNr.webp) 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](#integrations) [Section titled “Integrations”](#integrations) Crowdin uses the following IP address to interact with the integrations: ```shell 52.45.158.111/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 18.204.99.167/32 34.232.97.56/32 35.171.66.7/32 44.194.158.59/32 44.196.180.134/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.164.39.118/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-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-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-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. ![Shopify Apps](/_astro/shopify-apps-hero.UGYFhxKk_1dRSFf.webp) ## [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 ![AI Translations](/_astro/ai-translations.WU3ByWJQ_2iwLFB.webp) 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. | | 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 Machine Translation engines connected, as well as to create, 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 the list of Vendors. | | 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. | | Groups | `group` | Read and Write, Read only | Crowdin Enterprise only. Grants access to manage project groups a user has access to. | | Projects | `project` | Read and Write, Read only | Grants full access to manage project to which a user has access. | | Settings | `project.settings` | Read and Write, Read only | Grants access to project lists, permission to create, delete, and update project settings. | | 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 ass access to the file revisions. | | Webhooks | `project.webhook` | Read and Write, Read only | Grants access to read or write hook configurations on repositories a user manages. | | Translations | `project.translation` | Read and Write, Read only | Grants access to add new and manage existing translations. | | Dictionary | `project.dictionary` | Read and Write, Read only | Grants access to get and edit project dictionary. | | 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 retrieve the list of security logs and view individual security log entries. | | Fields | `field` | Read and Write, Read only | Crowdin Enterprise only. Grants access to organization fields, permission to create, delete, and update field settings. | | AI | `ai` | Read and Write, Read only | Grants access to AI, permission to create, delete, and update AI providers and prompts. | Caution The Vendors, Translation status, and Security logs 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 } } ```