User Interface
You can specify your own UI by specifying the uiPath property (and optionally fileName) or by using the React JSON schema forms.
React JSON Schema Forms
Section titled “React JSON Schema Forms”For all modules that have UI (except projectIntegration), you can use React JSON Schema forms as frontend.
To achieve this, you can specify the formSchema property in the module definition:
configuration.projectMenu = [ { formSchema: { "title": "A registration form", "description": "A simple form example.", "type": "object", "required": [ "firstName", "lastName" ], "properties": { "firstName": { "type": "string", "title": "First name", "default": "Chuck" }, "lastName": { "type": "string", "title": "Last name" } } }, formUiSchema: { "ui:submitButtonOptions": { "submitText": "Confirm Details" }, "lastName": { "ui:help": "Hint: Choose cool lastname!" } } }];By default, form data will be stored in app metadata. To retrieve data you can use this code:
crowdinApp.getMetadata(`${organizationId}-${projectId}`);To customize this behavior you can provide formGetDataUrl and formPostDataUrl properties that will contain URL to custom actions in your app. This actions should support GET request for fetching data (formGetDataUrl) and POST request to save new data (formPostDataUrl).
Response structure for GET request example:
res.status(200).send({ formData: { firstName: 'First Name' }}).end();Also, you can provide the following properties:
formSchema- new schema to replace active schema;formUiSchema- new uiSchema to replace current;message- custom message that will be displayed in toasts;redirect- url for redirect (useful for files download).
Custom HTML Field
Section titled “Custom HTML Field”You can add a piece of custom HTML to your form. To do this, you will need:
-
Add field with type
stringto the form with additionaluiSchemaproperties:const exampleConfig = {formSchema: {bio: {type: 'string'}},formUiSchema: {bio: {'ui:widget': 'htmlWidget'}}}; -
Provide form value using one of options -
ui:optionscontent orformData(formDatavalue have higher priority):const exampleConfig = {formSchema: {bio: {type: 'string',},},formUiSchema: {bio: {'ui:widget': 'htmlWidget',},},formData: {bio: '<h2>HTML formatted description</h2>',},};
Custom Widgets
Section titled “Custom Widgets”You can extend the functionality of your form by using custom widgets. Below are two examples of custom widgets that can be used to enhance your forms: crowdinFilesWidget and monacoEditorWidget.
Crowdin Files Widget
Section titled “Crowdin Files Widget”The crowdinFilesWidget widget allows users to select files. To use the crowdinFilesWidget, add a property to your formSchema with the type string and provide the custom widget in the formUiSchema as follows:
formSchema: { properties: { files: { title: 'Crowdin Files', type: 'string', }, },},formUiSchema: { files: { "ui:widget": 'crowdinFilesWidget', },}Monaco Editor Widget
Section titled “Monaco Editor Widget”The monacoEditorWidget allows you to embed a powerful code editor into your form. It is useful for scenarios where users need to provide or modify code snippets. The widget is based on the Monaco Editor, which powers Visual Studio Code, and supports syntax highlighting, auto-completion, and other advanced editor features.
formSchema: { properties: { code: { type: 'string', language: 'javascript', height: 500, minimap: true, lineNumbers: 'on', }, },},formUiSchema: { code: { "ui:widget": 'monacoEditorWidget', "ui:help": 'Provide your custom JavaScript code here', },}You can specify various editor options such as language, height, lineNumbers, and minimap visibility or provide custom options with different settings. You can find the full list of available options in the Monaco Editor documentation. For example:
options: { minimap: { enabled: true, side: 'left', }, lineNumbers: 'on',}Manipulate Form from JavaScript
Section titled “Manipulate Form from JavaScript”You may want to manipulate the form from JavaScript. For example, you may want to change the form schema or uiSchema from a custom code widget.
To do this, you can use the global renderForm function and the formSchema, formUiSchema and formData variables.
const updatedSchema = { ...window.currentFormSchema, properties: { ...window.currentFormSchema.properties, lastName: { type: 'string', title: 'New title for last name field', default: 'Jon Doe' } }};
window.renderForm(window.currentFormData, updatedSchema, window.currentFormUiSchema);Listening to Data Change Events
Section titled “Listening to Data Change Events”You can also listen for form data changes:
function setInputValue (target, value) { const setter = Object.getOwnPropertyDescriptor(target, "value").set const prototype = Object.getPrototypeOf(target) const prototypeValueSetter = Object.getOwnPropertyDescriptor(prototype, 'value').set if (setter && setter !== prototypeValueSetter) { prototypeValueSetter.call(target, value) } else { setter.call(target, value) } const event = new Event("input", {bubbles: true}) target.dispatchEvent(event);}
document.addEventListener('formDataUpdated', (event) => { if (event?.detail?.type === '1') { setInputValue(document.getElementById('root_selector'), 'value'); }});Masking Credentials
Section titled “Masking Credentials”The maskKey is designed to mask a given string, revealing only the last three characters while replacing the preceding characters with a masking symbol (by default, an asterisk *).
import crowdinModule from '@crowdin/app-project-module';
// ...
crowdinModule.maskKey('ABCD-ABCD-ABCD'); // Output: "***********BCD"You should mask input values if you use form to store credentials, api keys, passwords etc. To achieve this add middlewares before functions used to display and store form data. Also use password widget in form schema.
Example:
const moduleConfig = { formSchema: { title: "Service Setup Form", type: "object", required: ["key"], properties: { key: { type: "string", title: "API Key" } } }, formUiSchema: { key: { "ui:widget": "password" } }, formPostDataUrl: '/form'};import crowdinModule from '@crowdin/app-project-module';
// ...
app.post('/save-form-data', crowdinModule.postRequestCredentialsMasker(settingsForm), async (req, res) => { // Always specify moduleKey to restrict access to authorized modules const { client, context } = await crowdinApp.establishCrowdinConnection({ jwtToken: req.query.jwtToken, moduleKey: ['my-settings-module'] // Only this module can access });
const formData = req.body.data;});
// if your application uses a non-default action to retrieve data, you should also mask it.app.get('/get-form-data', crowdinModule.getRequestCredentialsMasker({ moduleConfig: settingsForm }), async (req, res) => { // Always specify moduleKey to restrict access to authorized modules const { client, context } = await crowdinApp.establishCrowdinConnection({ jwtToken: req.query.jwtToken, moduleKey: ['my-settings-module'] // Only this module can access });
const data = (await storage.getStorage().getMetadata(id)) || {};
return res.send({ formData: data });});With such setup API key will be masked for front-end. You can use it in your code as usual.
Skipping Toast After Form Submit
Section titled “Skipping Toast After Form Submit”To skip default toast you can return message: '', or message: null in response.
res.send({ message: null });Crowdin UI Registry
Section titled “Crowdin UI Registry”If you build a custom UI (the uiPath option), use the Crowdin UI Registry - a shadcn registry that serves the Crowdin design tokens and a curated, pinned mirror of shadcn/ui components. It helps your app look and feel native to Crowdin.
Add the registry to your components.json:
{ "registries": { "@crowdin": "https://crowdin.github.io/ui-registry/r/{name}.json" }}Start with the style, then add the components you need:
npx shadcn@latest add @crowdin/stylenpx shadcn@latest add @crowdin/button @crowdin/dialogAlso install @crowdin/app-theme - it bridges the host theme variables so your app automatically follows the theme selected in Crowdin, with fallbacks for local development.
Browse all available components in the registry showcase. The registry also works with the shadcn MCP server, so AI agents can browse and install components as well.
Theming
Section titled “Theming”For applications using React JSON Schema forms, the light or dark theme will be automatically set based on the theme chosen in Crowdin. For custom UIs built with the Crowdin UI Registry, the @crowdin/app-theme item follows the Crowdin theme automatically.
Manual Theme Selection for Other Applications
Section titled “Manual Theme Selection for Other Applications”For all other cases, you can use the following App JS method:
AP.getTheme(function (theme) { if (theme === 'dark') { // set dark styles } else { // set light styles }});You can also retrieve the list of CSS variables and use them for custom styling:
AP.getCssVariables(style => { // apply custom styling});Alternatively, you can use one of the predefined CSS classes:
crdn-text:primarycrdn-text:titlecrdn-text:bodycrdn-text:mutedcrdn-text:disabledcrdn-text:infocrdn-text:successcrdn-text:warningcrdn-text:dangercrdn-bg:lvl-0crdn-bg:lvl-1crdn-bg:lvl-2crdn-bg:lvl-2:05crdn-bg:lvl-3crdn-bg:primarycrdn-bg:successcrdn-bg:warningcrdn-bg:dangercrdn-border:primarycrdn-border:dangercrdn-border:infocrdn-border:successcrdn-border:warning