Skip to content

User Interface

You can specify your own UI by specifying the uiPath property (and optionally fileName) or by using the 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:

index.js
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).

You can add a piece of custom HTML to your form. To do this, you will need:

  1. Add field with type string to the form with additional uiSchema properties:

    const exampleConfig = {
    formSchema: {
    bio: {
    type: 'string'
    }
    },
    formUiSchema: {
    bio: {
    'ui:widget': 'htmlWidget'
    }
    }
    };
  2. Provide form value using one of options - ui:options content or formData (formData value have higher priority):

    const exampleConfig = {
    formSchema: {
    bio: {
    type: 'string',
    },
    },
    formUiSchema: {
    bio: {
    'ui:widget': 'htmlWidget',
    },
    },
    formData: {
    bio: '<h2>HTML formatted description</h2>',
    },
    };

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.

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:

index.js
formSchema: {
properties: {
files: {
title: 'Crowdin Files',
type: 'string',
},
},
},
formUiSchema: {
files: {
"ui:widget": 'crowdinFilesWidget',
},
}

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.

index.js
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:

index.js
options: {
minimap: {
enabled: true,
side: 'left',
},
lineNumbers: 'on',
}

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.

index.js
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);

You can also listen for form data changes:

index.js
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');
}
});

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 *).

index.js
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:

index.js
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'
};
index.js
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.

To skip default toast you can return message: '', or message: null in response.

res.send({ message: null });

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:

components.json
{
"registries": {
"@crowdin": "https://crowdin.github.io/ui-registry/r/{name}.json"
}
}

Start with the style, then add the components you need:

Terminal window
npx shadcn@latest add @crowdin/style
npx shadcn@latest add @crowdin/button @crowdin/dialog

Also 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.

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:

Text colors
crdn-text:primary
crdn-text:title
crdn-text:body
crdn-text:muted
crdn-text:disabled
crdn-text:info
crdn-text:success
crdn-text:warning
crdn-text:danger
Backgrounds
crdn-bg:lvl-0
crdn-bg:lvl-1
crdn-bg:lvl-2
crdn-bg:lvl-2:05
crdn-bg:lvl-3
crdn-bg:primary
crdn-bg:success
crdn-bg:warning
crdn-bg:danger
Borders
crdn-border:primary
crdn-border:danger
crdn-border:info
crdn-border:success
crdn-border:warning