@crowdin/app-project-module 0.28.0-13 → 0.28.0-2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +1 -19
- package/README.md +984 -1
- package/out/handlers/crowdin-webhook.js +1 -1
- package/out/handlers/custom-file-format/download.d.ts +4 -0
- package/out/handlers/{file-processing/file-download.js → custom-file-format/download.js} +2 -3
- package/out/handlers/{file-processing/custom-file-format.js → custom-file-format/process.js} +23 -8
- package/out/handlers/form-data-display.js +2 -6
- package/out/handlers/form-data-save.js +1 -5
- package/out/handlers/manifest.js +0 -36
- package/out/index.d.ts +3 -3
- package/out/index.js +26 -49
- package/out/middlewares/render-ui-module.js +1 -3
- package/out/models/index.d.ts +28 -71
- package/out/models/index.js +0 -4
- package/out/static/js/form.js +9 -9
- package/out/storage/index.js +7 -4
- package/out/util/cron.js +1 -5
- package/out/util/defaults.d.ts +2 -3
- package/out/util/defaults.js +4 -21
- package/out/util/index.js +1 -5
- package/out/util/webhooks.js +4 -15
- package/out/views/form.handlebars +2 -6
- package/package.json +15 -15
- package/rollup.config.mjs +31 -0
- package/out/handlers/file-processing/file-download.d.ts +0 -4
- package/out/handlers/file-processing/pre-post-process.d.ts +0 -4
- package/out/handlers/file-processing/pre-post-process.js +0 -99
- package/out/util/files.d.ts +0 -3
- package/out/util/files.js +0 -43
- /package/out/handlers/{file-processing/custom-file-format.d.ts → custom-file-format/process.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -2,7 +2,990 @@
|
|
|
2
2
|
|
|
3
3
|
Module that will automatically add all necessary endpoints for Crowdin App.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
It either can extends your [Express](http://expressjs.com/) application or even create it for you.
|
|
6
|
+
|
|
7
|
+
Module expose two main methods:
|
|
8
|
+
|
|
9
|
+
- `createApp` to fully create for you Express application
|
|
10
|
+
- `addCrowdinEndpoints` to extend your Express application
|
|
11
|
+
|
|
12
|
+
In both options you will need to provide Crowdin App configuration file. Please refer to jsdoc for more details.
|
|
13
|
+
|
|
14
|
+
`addCrowdinEndpoints` will return an object with several methods to help you add your own custom logic [see example](#resources).
|
|
15
|
+
|
|
16
|
+
- `saveMetadata` to save metadata (may be associated with an organization, project, etc)
|
|
17
|
+
- `getMetadata` to get metadata
|
|
18
|
+
- `deleteMetadata` to delete metadata (usually useful in `onUninstall` hook)
|
|
19
|
+
- `getUserSettings` to get settings that users manage in the integration module
|
|
20
|
+
- `establishCrowdinConnection` method that accept jwt token that you may forward from module UI and it will return back Crowdin client instance and the context.
|
|
21
|
+
|
|
22
|
+
## Table of Contents
|
|
23
|
+
|
|
24
|
+
- [Installation](#installation)
|
|
25
|
+
- [Sample Project Integration App](#sample-project-integration-app)
|
|
26
|
+
- [Payment](#payment)
|
|
27
|
+
- [Authorization](#authorization)
|
|
28
|
+
- [Custom login form](#customize-your-app-login-form)
|
|
29
|
+
- [OAuth2 login](#oauth2-support)
|
|
30
|
+
- [Storage](#storage)
|
|
31
|
+
- [SQLite](#sqlite)
|
|
32
|
+
- [PostgreSQL](#postgresql)
|
|
33
|
+
- [MySQL](#mysql)
|
|
34
|
+
- [Settings window](#settings-window)
|
|
35
|
+
- [Info window](#info-window)
|
|
36
|
+
- [Background tasks](#background-tasks)
|
|
37
|
+
- [Errors handling](#errors-handling)
|
|
38
|
+
- [Error propagation](#error-propagation)
|
|
39
|
+
- [Error interceptor](#error-interceptor)
|
|
40
|
+
- [Debug mode](#debug-mode)
|
|
41
|
+
- [Modules](#modules)
|
|
42
|
+
- [Custom File Format](#custom-file-format)
|
|
43
|
+
- [Custom MT](#custom-mt)
|
|
44
|
+
- [Profile Resources Menu](#profile-resources-menu)
|
|
45
|
+
- [Other modules](#other-modules)
|
|
46
|
+
- [Other options](#other-options)
|
|
47
|
+
- [Contributing](#contributing)
|
|
48
|
+
- [Seeking Assistance](#seeking-assistance)
|
|
49
|
+
- [License](#license)
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
### npm
|
|
54
|
+
|
|
55
|
+
`npm i @crowdin/app-project-module`
|
|
56
|
+
|
|
57
|
+
### yarn
|
|
58
|
+
|
|
59
|
+
`yarn add @crowdin/app-project-module`
|
|
60
|
+
|
|
61
|
+
## Sample Project Integration App
|
|
62
|
+
|
|
63
|
+
```javascript
|
|
64
|
+
const crowdinModule = require('@crowdin/app-project-module');
|
|
65
|
+
const crowdinAppFunctions = require('@crowdin/crowdin-apps-functions');
|
|
66
|
+
const axios = require('axios').default;
|
|
67
|
+
|
|
68
|
+
const configuration = {
|
|
69
|
+
baseUrl: 'https://123.ngrok.io',
|
|
70
|
+
clientId: 'clientId',
|
|
71
|
+
clientSecret: 'clientSecret',
|
|
72
|
+
scopes: [
|
|
73
|
+
crowdinModule.Scope.PROJECTS,
|
|
74
|
+
crowdinModule.Scope.TRANSLATION_MEMORIES
|
|
75
|
+
],
|
|
76
|
+
name: 'Sample App',
|
|
77
|
+
identifier: 'sample-app',
|
|
78
|
+
description: 'Sample App description',
|
|
79
|
+
dbFolder: __dirname,
|
|
80
|
+
imagePath: __dirname + '/' + 'logo.png',
|
|
81
|
+
crowdinUrls: { // custom urls to override
|
|
82
|
+
// apiUrl: 'https://<copy_name>.crowdin.dev/api/v2', // 'https://<org_name>.<copy_name>.crowdin.dev/api/v2' for enterprise
|
|
83
|
+
// accountUrl: 'https://accounts.<copy_name>.crowdin.dev/oauth/token', // (default https://accounts.crowdin.com/oauth/token)
|
|
84
|
+
// subscriptionUrl: 'https://<copy_name>.crowdin.dev' // (default https://crowdin.com or https://org.api.crowdin.com)
|
|
85
|
+
},
|
|
86
|
+
projectIntegration: {
|
|
87
|
+
withRootFolder: true,
|
|
88
|
+
withCronSync: {
|
|
89
|
+
crowdin: true,
|
|
90
|
+
integration: true,
|
|
91
|
+
},
|
|
92
|
+
webhooks: { //enable webhook
|
|
93
|
+
crowdinWebhookUrl: '/notify', // override crowdin webhook endpoint
|
|
94
|
+
integrationWebhookUrl: '/update-entity', // override integration webhook endpoint
|
|
95
|
+
urlParam: 'information', // override request param name
|
|
96
|
+
crowdinWebhooks: async (client, projectId, available, appSettings) => { // optional
|
|
97
|
+
// here you need to create crowdin webhooks if the default doesn't work for you
|
|
98
|
+
// used with crowdinWebhookInterceptor
|
|
99
|
+
await client.webhooksApi.addWebhook(projectId, {
|
|
100
|
+
'Application webhook',
|
|
101
|
+
url: 'https://123.ngrok.io/notify',
|
|
102
|
+
events: ['file.translated'],
|
|
103
|
+
requestType: 'POST',
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
crowdinWebhookInterceptor: async (projectId, client, appRootFolder, appSettings, syncSettings) => { // optional
|
|
107
|
+
// used with crowdinWebhooks
|
|
108
|
+
return {
|
|
109
|
+
'101': ['de', 'fr'],
|
|
110
|
+
'103': ['de'],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
integrationWebhooks: async (credentials, urlParam, available, appSettings, syncSettings) => {
|
|
114
|
+
// here you need to create webhooks on the integration side
|
|
115
|
+
// used with integrationWebhookInterceptor or RabbitMQ queue
|
|
116
|
+
await axios.post('https://site.com/api/webhooks');
|
|
117
|
+
},
|
|
118
|
+
integrationWebhookInterceptor: async (projectId, client, rootFolder, appSettings, syncSettings, webhookRequest) => {
|
|
119
|
+
// used with integrationWebhooks
|
|
120
|
+
return [
|
|
121
|
+
{
|
|
122
|
+
id: '47282999980',
|
|
123
|
+
name: 'Home page',
|
|
124
|
+
parentId: '1',
|
|
125
|
+
type: 'json',
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: '73291251883',
|
|
129
|
+
name: 'Intro',
|
|
130
|
+
parentId: '2',
|
|
131
|
+
type: 'json',
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
},
|
|
135
|
+
queueUrl: 'amqp://localhost:5672', // RabbitMQ url to listen to the webhook queue,
|
|
136
|
+
},
|
|
137
|
+
integrationPagination: true, //enable pagination on integration files list
|
|
138
|
+
integrationOneLevelFetching: true, //turn on request when opening a directory and pass its id
|
|
139
|
+
integrationSearchListener: true, //turn on search listener and pass search string
|
|
140
|
+
uploadTranslations: true, //enable the option to download translations from integration.
|
|
141
|
+
getIntegrationFiles: async (credentials, appSettings, parentId, search, page) => {
|
|
142
|
+
//here you need to fetch files/objects from integration
|
|
143
|
+
const res = [
|
|
144
|
+
{
|
|
145
|
+
id: '12',
|
|
146
|
+
name: 'File from integration',
|
|
147
|
+
type: 'json',
|
|
148
|
+
parentId: '10'
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: '14',
|
|
152
|
+
name: 'File from integration2',
|
|
153
|
+
type: 'xml',
|
|
154
|
+
parentId: '11'
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
id: '10',
|
|
158
|
+
name: 'Folder from integration'
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: '11',
|
|
162
|
+
name: 'Folder from integratio2'
|
|
163
|
+
customContent: `<div style='color: red;'>Custom Folder name</div>`,
|
|
164
|
+
parentId: '1'
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
id: '1',
|
|
168
|
+
name: 'Branch from integratio',
|
|
169
|
+
nodeType: '2'
|
|
170
|
+
},
|
|
171
|
+
];
|
|
172
|
+
return {
|
|
173
|
+
data: res,
|
|
174
|
+
message: 'Test',
|
|
175
|
+
stopPagination: true, //send if no next files page
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
getFileProgress: async (projectId, client, fileId) => { //Use this function if you require a customized translation progress.
|
|
179
|
+
return {
|
|
180
|
+
[fileId]: [
|
|
181
|
+
{
|
|
182
|
+
languageId: 'uk',
|
|
183
|
+
eTag: '49a76a1632973b00175dac942976f7c6',
|
|
184
|
+
words: {
|
|
185
|
+
total: 180,
|
|
186
|
+
translated: 0,
|
|
187
|
+
approved: 0
|
|
188
|
+
},
|
|
189
|
+
phrases: {
|
|
190
|
+
total: 6,
|
|
191
|
+
translated: 0,
|
|
192
|
+
approved: 0
|
|
193
|
+
},
|
|
194
|
+
translationProgress: 100,
|
|
195
|
+
approvalProgress: 0
|
|
196
|
+
}
|
|
197
|
+
]
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
updateCrowdin: async (projectId, client, credentials, request, rootFolder, appSettings, uploadTranslations) => {
|
|
201
|
+
//here you need to get data from integration and upload it to Crowdin
|
|
202
|
+
console.log(`Request for updating data in Crowdin ${JSON.stringify(request)}`);
|
|
203
|
+
const directories = await client.sourceFilesApi
|
|
204
|
+
.withFetchAll()
|
|
205
|
+
.listProjectDirectories(projectId);
|
|
206
|
+
const { folder, files } = await crowdinAppFunctions.getOrCreateFolder({
|
|
207
|
+
directories: directories.data.map((d) => d.data),
|
|
208
|
+
client,
|
|
209
|
+
projectId,
|
|
210
|
+
directoryName: 'Folder from integration',
|
|
211
|
+
parentDirectory: rootFolder
|
|
212
|
+
});
|
|
213
|
+
const fileContent = {
|
|
214
|
+
title: 'Hello World',
|
|
215
|
+
};
|
|
216
|
+
const fileName = 'integration.json';
|
|
217
|
+
const fileId = await crowdinAppFunctions.updateOrCreateFile({
|
|
218
|
+
client,
|
|
219
|
+
projectId,
|
|
220
|
+
name: fileName,
|
|
221
|
+
title: 'Sample file from integration',
|
|
222
|
+
type: 'json',
|
|
223
|
+
directoryId: folder.id,
|
|
224
|
+
data: fileContent,
|
|
225
|
+
file: files.find((f) => f.name === 'integration.json'),
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
if (uploadTranslations) {
|
|
229
|
+
const fileTranslation = {
|
|
230
|
+
title: 'Hola Mundo',
|
|
231
|
+
};
|
|
232
|
+
await crowdinAppFunctions.uploadTranslations(
|
|
233
|
+
client,
|
|
234
|
+
projectId,
|
|
235
|
+
fileId,
|
|
236
|
+
'es',
|
|
237
|
+
fileName,
|
|
238
|
+
fileTranslation,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
message: 'Some message',
|
|
244
|
+
};
|
|
245
|
+
},
|
|
246
|
+
updateIntegration: async (projectId, client, credentials, request, rootFolder, appSettings) => {
|
|
247
|
+
//here should be logic to get translations from Crowdin and upload them to integration
|
|
248
|
+
console.log(`Request for updating data in Integration ${JSON.stringify(request)}`);
|
|
249
|
+
const directories = await client.sourceFilesApi
|
|
250
|
+
.withFetchAll()
|
|
251
|
+
.listProjectDirectories(projectId);
|
|
252
|
+
const { files } = await crowdinAppFunctions.getFolder({
|
|
253
|
+
directories: directories.data.map((d) => d.data),
|
|
254
|
+
client,
|
|
255
|
+
projectId,
|
|
256
|
+
directoryName: 'Folder from integration',
|
|
257
|
+
parentDirectory: rootFolder
|
|
258
|
+
});
|
|
259
|
+
const file = files.find((f) => f.name === 'integration.json');
|
|
260
|
+
if (file) {
|
|
261
|
+
const translationsLink =
|
|
262
|
+
await client.translationsApi.buildProjectFileTranslation(
|
|
263
|
+
projectId,
|
|
264
|
+
file.id,
|
|
265
|
+
{ targetLanguageId: 'uk' },
|
|
266
|
+
);
|
|
267
|
+
if (!translationsLink) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const response = await axios.get(translationsLink.data.url);
|
|
271
|
+
console.log(response.data);
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
onLogout: async (projectId, client, credentials, appSettings) => {
|
|
275
|
+
//cleanup logic
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
crowdinModule.createApp(configuration);
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## Payment
|
|
284
|
+
|
|
285
|
+
By default App does not have any subscription and it's free to use. But you can override this.
|
|
286
|
+
|
|
287
|
+
```javascript
|
|
288
|
+
configuration.pricing = {
|
|
289
|
+
plantType: 'recurring',
|
|
290
|
+
trial: 14, //amount of days to use app for free
|
|
291
|
+
trialCrowdin: 14, //amount of days specifically in crowdin workspace
|
|
292
|
+
trialEnterprise: 30, //amount of days specifically for enterprise
|
|
293
|
+
cachingSeconds: 12 * 60 * 60, //time in seconds of how long to cache subscription info
|
|
294
|
+
infoDisplayDaysThreshold: 14 //number of days threshold to check if subscription info should be displayed (if not defined then info will be always visible)
|
|
295
|
+
};
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Authorization
|
|
299
|
+
|
|
300
|
+
### Customize your app login form
|
|
301
|
+
|
|
302
|
+
By default, login page for your app will require only to enter `apiToken` to communicate with third party service.
|
|
303
|
+
But there is also a possibility to customize it.
|
|
304
|
+
|
|
305
|
+
```javascript
|
|
306
|
+
configuration.projectIntegration.loginForm = {
|
|
307
|
+
fields: [
|
|
308
|
+
{
|
|
309
|
+
key: 'username',
|
|
310
|
+
label: 'Username',
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
key: 'password',
|
|
314
|
+
label: 'Password',
|
|
315
|
+
type: 'password'
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
label: 'Api creds',
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
helpText: 'Api Key for http requests',
|
|
322
|
+
key: 'apiKey',
|
|
323
|
+
label: 'Api Key'
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
key: 'server',
|
|
327
|
+
label: 'Data center',
|
|
328
|
+
type: 'select',
|
|
329
|
+
defaultValue: '1',
|
|
330
|
+
options: [
|
|
331
|
+
{
|
|
332
|
+
value: '1',
|
|
333
|
+
label: 'USA'
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
value: '2',
|
|
337
|
+
label: 'EU'
|
|
338
|
+
}
|
|
339
|
+
]
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
key: 'apiKey',
|
|
343
|
+
label: 'Api key',
|
|
344
|
+
type: 'textarea'
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
key: 'fileSecret',
|
|
348
|
+
label: 'Upload file',
|
|
349
|
+
type: 'file',
|
|
350
|
+
accept: '.txt'
|
|
351
|
+
},
|
|
352
|
+
]
|
|
353
|
+
};
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### OAuth2 support
|
|
357
|
+
|
|
358
|
+
In case if third party service uses OAuth2 for authorization use `oauthLogin` field to configure it.
|
|
359
|
+
`loginForm` **in this case should remain undefined**.
|
|
360
|
+
|
|
361
|
+
Github example:
|
|
362
|
+
|
|
363
|
+
```javascript
|
|
364
|
+
configuration.projectIntegration.oauthLogin = {
|
|
365
|
+
authorizationUrl: 'https://github.com/login/oauth/authorize',
|
|
366
|
+
clientId: 'github_app_client_id',
|
|
367
|
+
clientSecret: 'github_app_client_secret',
|
|
368
|
+
accessTokenUrl: 'https://github.com/login/oauth/access_token'
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Google example:
|
|
373
|
+
|
|
374
|
+
```javascript
|
|
375
|
+
configuration.projectIntegration.oauthLogin = {
|
|
376
|
+
scope: 'https%3A//www.googleapis.com/auth/userinfo.email',
|
|
377
|
+
authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
|
|
378
|
+
clientId: 'google_web_app_client_id',
|
|
379
|
+
clientSecret: 'google_web_app_client_secret',
|
|
380
|
+
accessTokenUrl: 'https://oauth2.googleapis.com/token',
|
|
381
|
+
extraAutorizationUrlParameters: {
|
|
382
|
+
response_type: 'code',
|
|
383
|
+
access_type: 'offline',
|
|
384
|
+
prompt: 'consent'
|
|
385
|
+
},
|
|
386
|
+
extraAccessTokenParameters: {
|
|
387
|
+
grant_type: 'authorization_code'
|
|
388
|
+
},
|
|
389
|
+
extraRefreshTokenParameters: {
|
|
390
|
+
grant_type: 'refresh_token'
|
|
391
|
+
},
|
|
392
|
+
refresh: true
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
The `oauthLogin` property allows you to customize many different properties, mappings, etc. So that you can integrate with any OAuth2 implementation.
|
|
397
|
+
Main default values:
|
|
398
|
+
|
|
399
|
+
- redirect uri prefix will be `/oauth/code`
|
|
400
|
+
- client id field name in url parameters and in request payload will be `client_id`
|
|
401
|
+
- client secret field name in request payload will be `client_secret`
|
|
402
|
+
- access token field name should be `access_token`
|
|
403
|
+
- be default assumption is that token do not have any expiration date, to change this behavior use `refresh` flag so then refresh token and expires in will be taken into consideration
|
|
404
|
+
- access token field name should be `refresh_token`
|
|
405
|
+
- expires in field name should be `expires_in` (value should be in seconds)
|
|
406
|
+
|
|
407
|
+
This module rely that OAuth2 protocol is implemented by third party service in this way:
|
|
408
|
+
|
|
409
|
+
- request for access token should be done via POST request to `accessTokenUrl` with JSON body that will contain at least `clientId`, `clientSecret`, `code` and `redirectUri` (also possible to add extra fields via `extraAccessTokenParameters` property)
|
|
410
|
+
- request to refresh token should be done via POST request to `accessTokenUrl` (or `refreshTokenUrl` if defined) with JSON body that will contain at least `clientId`, `clientSecret` and `refreshToken` (also possible to add extra fields via `extraRefreshTokenParameters` property)
|
|
411
|
+
- both requests will return JSON response with body that contains `accessToken` and, if enabled, `refreshToken` (optional) and `expireIn`
|
|
412
|
+
|
|
413
|
+
To override those requests please use `performGetTokenRequest` and `performRefreshTokenRequest` (e.g. when requests should be done with different HTTP methods or data should be tranfered as query string or form data).
|
|
414
|
+
|
|
415
|
+
Mailup example:
|
|
416
|
+
|
|
417
|
+
```javascript
|
|
418
|
+
const clientId = 'client_id';
|
|
419
|
+
const clientSecret = 'client_secret';
|
|
420
|
+
const tokenUrl = 'https://services.mailup.com/Authorization/OAuth/Token';
|
|
421
|
+
|
|
422
|
+
configuration.projectIntegration.oauthLogin = {
|
|
423
|
+
authorizationUrl: 'https://services.mailup.com/Authorization/OAuth/LogOn',
|
|
424
|
+
clientId,
|
|
425
|
+
clientSecret,
|
|
426
|
+
extraAutorizationUrlParameters: {
|
|
427
|
+
response_type: 'code'
|
|
428
|
+
},
|
|
429
|
+
refresh: true,
|
|
430
|
+
performGetTokenRequest: async (code, query, url) => {
|
|
431
|
+
//query is an object with all query params
|
|
432
|
+
//url is an url string that OAuth server used to call us back
|
|
433
|
+
const url = `${tokenUrl}?code=${code}&grant_type=authorization_code`;
|
|
434
|
+
const headers = {
|
|
435
|
+
'Authorization': `Bearer ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`
|
|
436
|
+
};
|
|
437
|
+
return (await axios.get(url, { headers })).data;
|
|
438
|
+
},
|
|
439
|
+
performRefreshTokenRequest: async (credentials) => {
|
|
440
|
+
const params = {
|
|
441
|
+
refresh_token: credentials.refreshToken,
|
|
442
|
+
grant_type: 'refresh_token',
|
|
443
|
+
client_id: clientId,
|
|
444
|
+
client_secret: clientSecret
|
|
445
|
+
};
|
|
446
|
+
const data = Object.keys(params)
|
|
447
|
+
.map((key) => `${key}=${encodeURIComponent(params[key])}`)
|
|
448
|
+
.join('&');
|
|
449
|
+
const headers = {
|
|
450
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
451
|
+
};
|
|
452
|
+
return (await axios.post(tokenUrl, data, { headers })).data;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
In addition you can specify extra fields on login screen that you can use to dynamically build authorization url.
|
|
458
|
+
|
|
459
|
+
```javascript
|
|
460
|
+
configuration.projectIntegration.oauthLogin.loginFields = [
|
|
461
|
+
{
|
|
462
|
+
key: 'region',
|
|
463
|
+
label: 'Region',
|
|
464
|
+
type: 'select',
|
|
465
|
+
defaultValue: 'NA',
|
|
466
|
+
options: [
|
|
467
|
+
{
|
|
468
|
+
value: 'NA',
|
|
469
|
+
label: 'North American'
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
value: 'EU',
|
|
473
|
+
label: 'Europe'
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
value: 'AZURE_NA',
|
|
477
|
+
label: 'Azure North American'
|
|
478
|
+
}
|
|
479
|
+
]
|
|
480
|
+
}
|
|
481
|
+
];
|
|
482
|
+
|
|
483
|
+
configuration.projectIntegration.oauthLogin.getAuthorizationUrl = (redirectUrl, loginForm) => {
|
|
484
|
+
const region = loginForm.region;
|
|
485
|
+
if (region === 'EU') {
|
|
486
|
+
return `https://eu-region.com?client_id=<client-id>&redirect_uri=${redirectUrl}`;
|
|
487
|
+
} else {
|
|
488
|
+
return `https://default-region.com?client_id=<client-id>&redirect_uri=${redirectUrl}`;
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
Please refer to jsdoc for more details.
|
|
494
|
+
|
|
495
|
+
## Storage
|
|
496
|
+
|
|
497
|
+
Module can be configured to use following storages:
|
|
498
|
+
|
|
499
|
+
### SQLite
|
|
500
|
+
|
|
501
|
+
```javascript
|
|
502
|
+
//specify folder where sqlite db file will be located
|
|
503
|
+
configuration.dbFolder = __dirname;
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### PostgreSQL
|
|
507
|
+
|
|
508
|
+
```javascript
|
|
509
|
+
configuration.postgreConfig = {
|
|
510
|
+
host: 'localhost',
|
|
511
|
+
user: 'postgres',
|
|
512
|
+
password: 'password',
|
|
513
|
+
database: 'test'
|
|
514
|
+
};
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
### MySQL
|
|
518
|
+
|
|
519
|
+
```javascript
|
|
520
|
+
configuration.mysqlConfig = {
|
|
521
|
+
host: 'localhost',
|
|
522
|
+
user: 'root',
|
|
523
|
+
password: 'password',
|
|
524
|
+
database: 'test'
|
|
525
|
+
};
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
## Settings window
|
|
529
|
+
|
|
530
|
+
It is also possible to define settings window for your app where users can customize integration flow.
|
|
531
|
+
|
|
532
|
+
```javascript
|
|
533
|
+
configuration.projectIntegration.getConfiguration = (projectId, crowdinClient, integrationCredentials) => {
|
|
534
|
+
return [
|
|
535
|
+
{
|
|
536
|
+
label: 'GENERAL'
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
key: 'flag',
|
|
540
|
+
label: 'Checkbox',
|
|
541
|
+
type: 'checkbox'
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
key: 'text',
|
|
545
|
+
label: 'Text input',
|
|
546
|
+
type: 'text',
|
|
547
|
+
helpText: 'Help text'
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
key: 'option',
|
|
551
|
+
label: 'Select',
|
|
552
|
+
type: 'select',
|
|
553
|
+
defaultValue: '12',
|
|
554
|
+
isSearchable: true, //allow to search for option(s), default is `false`
|
|
555
|
+
isMulti: true, //allow to select multiple options, default is `false`
|
|
556
|
+
options: [
|
|
557
|
+
{
|
|
558
|
+
value: '12',
|
|
559
|
+
label: 'Option'
|
|
560
|
+
}
|
|
561
|
+
]
|
|
562
|
+
}
|
|
563
|
+
]
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
//auto reload on settings updates
|
|
567
|
+
configuration.projectIntegration.reloadOnConfigSave = true;
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
## Info window
|
|
571
|
+
|
|
572
|
+
You also can define section with some information notes or help section for your app.
|
|
573
|
+
|
|
574
|
+
```javascript
|
|
575
|
+
configuration.projectIntegration.infoModal = {
|
|
576
|
+
title: 'Info',
|
|
577
|
+
content: `
|
|
578
|
+
<h1>This is your app help section</h1>
|
|
579
|
+
</br>
|
|
580
|
+
<h2>This is just an example</h2>
|
|
581
|
+
`
|
|
582
|
+
}
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
## Background tasks
|
|
586
|
+
|
|
587
|
+
In order to register background tasks that app will invoke periodically invoke you can use `cronJobs` field.
|
|
588
|
+
|
|
589
|
+
```javascript
|
|
590
|
+
configuration.projectIntegration.cronJobs = [
|
|
591
|
+
{
|
|
592
|
+
//every 10 seconds
|
|
593
|
+
expression: '*/10 * * * * *',
|
|
594
|
+
task: (projectId, client, apiCredentials, appRootFolder, config) => {
|
|
595
|
+
console.log(`Running background task for project : ${projectId}`);
|
|
596
|
+
console.log(`Api credentials : ${JSON.stringify(apiCredentials)}`);
|
|
597
|
+
console.log(`App config : ${JSON.stringify(config)}`);
|
|
598
|
+
console.log(appRootFolder ? JSON.stringify(appRootFolder) : 'No root folder');
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
]
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
For cron syntax guide please refer to this [documentation](https://github.com/node-cron/node-cron#cron-syntax).
|
|
605
|
+
|
|
606
|
+
## Errors Handling
|
|
607
|
+
|
|
608
|
+
### Error propagation
|
|
609
|
+
|
|
610
|
+
In case if something is wrong with app settings or credentials are invalid you can throw an explanation message that will be then visible on the UI side.
|
|
611
|
+
e.g. check if entered credentials are valid:
|
|
612
|
+
|
|
613
|
+
```javascript
|
|
614
|
+
configuration.projectIntegration.checkConnection = (credentials) => {
|
|
615
|
+
if (!credentials.password || credentials.password.length < 6) {
|
|
616
|
+
throw 'Password is too weak';
|
|
617
|
+
}
|
|
618
|
+
//or call an service API with those credentials and check if request will be successful
|
|
619
|
+
};
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
Or if you need to manually control users liveness session you can throw an error with `401` code then your app will automatically do a log out action.
|
|
623
|
+
e.g. when your service has some specific session duration timeout or extra conditions which are not covered by this framework
|
|
624
|
+
|
|
625
|
+
```javascript
|
|
626
|
+
configuration.projectIntegration.getIntegrationFiles = async (credentials, appSettings) => {
|
|
627
|
+
//do a request/custom logic here
|
|
628
|
+
const sessionStillValid = false;
|
|
629
|
+
if (!sessionStillValid) {
|
|
630
|
+
throw {
|
|
631
|
+
message: 'session expired',
|
|
632
|
+
code: 401
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
//business logic
|
|
636
|
+
}
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
### Error interceptor
|
|
640
|
+
|
|
641
|
+
You can also provide interceptor to catch errors and process them (e.g. to log them in the centralized place).
|
|
642
|
+
|
|
643
|
+
```javascript
|
|
644
|
+
const Sentry = require('@sentry/node');
|
|
645
|
+
|
|
646
|
+
Sentry.init({
|
|
647
|
+
dsn: "https://examplePublicKey@o0.ingest.sentry.io/0",
|
|
648
|
+
tracesSampleRate: 1.0,
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
configuration.onError = (error) => {
|
|
652
|
+
Sentry.captureException(e);
|
|
653
|
+
};
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
### Debug mode
|
|
657
|
+
|
|
658
|
+
Also you can turn on the debug mode and application will log everything (useful of debugging).
|
|
659
|
+
|
|
660
|
+
```javascript
|
|
661
|
+
configuration.logger = {
|
|
662
|
+
enabled: true
|
|
663
|
+
};
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
Or even you can pass you own function to log messages (e.g. send them to external monitoring system).
|
|
667
|
+
|
|
668
|
+
```javascript
|
|
669
|
+
configuration.logger = {
|
|
670
|
+
enabled: true,
|
|
671
|
+
log: (message) => {
|
|
672
|
+
console.log(message);
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
## Modules
|
|
678
|
+
|
|
679
|
+
### Custom File Format
|
|
680
|
+
|
|
681
|
+
Example of [custom file format module](https://support.crowdin.com/crowdin-apps-modules/#custom-file-format-module).
|
|
682
|
+
|
|
683
|
+
```javascript
|
|
684
|
+
const crowdinModule = require('@crowdin/app-project-module');
|
|
685
|
+
const convert = require('xml-js');
|
|
686
|
+
|
|
687
|
+
const configuration = {
|
|
688
|
+
baseUrl: 'https://123.ngrok.io',
|
|
689
|
+
clientId: 'clientId',
|
|
690
|
+
clientSecret: 'clientSecret',
|
|
691
|
+
name: 'Sample App',
|
|
692
|
+
identifier: 'sample-app',
|
|
693
|
+
description: 'Sample App description',
|
|
694
|
+
dbFolder: __dirname,
|
|
695
|
+
imagePath: __dirname + '/' + 'logo.png',
|
|
696
|
+
customFileFormat: {
|
|
697
|
+
filesFolder: __dirname,
|
|
698
|
+
type: 'type-xyz',
|
|
699
|
+
multilingual: false,
|
|
700
|
+
autoUploadTranslations: true, //useful when single language format
|
|
701
|
+
signaturePatterns: {
|
|
702
|
+
fileName: '^.+\.xml$'
|
|
703
|
+
},
|
|
704
|
+
customSrxSupported: true,
|
|
705
|
+
parseFile: async (file, req, client, context, projectId) => {
|
|
706
|
+
const xml = convert.xml2json(file, { compact: true, spaces: 4 });
|
|
707
|
+
const fileContent = JSON.parse(xml);
|
|
708
|
+
//parse logic
|
|
709
|
+
const strings = [];
|
|
710
|
+
return { strings, error: 'Some error message' };
|
|
711
|
+
},
|
|
712
|
+
buildFile: async (file, req, strings, client, context, projectId) => {
|
|
713
|
+
const xml = convert.xml2json(file, { compact: true, spaces: 4 });
|
|
714
|
+
const fileContent = JSON.parse(xml);
|
|
715
|
+
//build logic
|
|
716
|
+
const contentFile = convert.json2xml(
|
|
717
|
+
fileContent,
|
|
718
|
+
{
|
|
719
|
+
compact: true,
|
|
720
|
+
ignoreComment: false,
|
|
721
|
+
spaces: 4
|
|
722
|
+
}
|
|
723
|
+
);
|
|
724
|
+
return { contentFile }
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
crowdinModule.createApp(configuration);
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
Also custom file format module can support strings export.
|
|
733
|
+
|
|
734
|
+
```javascript
|
|
735
|
+
const crowdinModule = require('@crowdin/app-project-module');
|
|
736
|
+
const convert = require('xml-js');
|
|
737
|
+
|
|
738
|
+
const configuration = {
|
|
739
|
+
baseUrl: 'https://123.ngrok.io',
|
|
740
|
+
clientId: 'clientId',
|
|
741
|
+
clientSecret: 'clientSecret',
|
|
742
|
+
name: 'Sample App',
|
|
743
|
+
identifier: 'sample-app',
|
|
744
|
+
description: 'Sample App description',
|
|
745
|
+
dbFolder: __dirname,
|
|
746
|
+
imagePath: __dirname + '/' + 'logo.png',
|
|
747
|
+
customFileFormat: {
|
|
748
|
+
filesFolder: __dirname,
|
|
749
|
+
type: 'type-xyz',
|
|
750
|
+
stringsExport: true,
|
|
751
|
+
multilingualExport: true,
|
|
752
|
+
extensions: [
|
|
753
|
+
'.resx'
|
|
754
|
+
],
|
|
755
|
+
exportStrings: async (req, strings, client, context, projectId) => {
|
|
756
|
+
const file = req.file;
|
|
757
|
+
//export logic
|
|
758
|
+
return { contentFile: '' }
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
crowdinModule.createApp(configuration);
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
By default custom file format will use `filesFolder` as a temporary folder to store huge responses. But it can be overridden:
|
|
767
|
+
|
|
768
|
+
```javascript
|
|
769
|
+
configuration.customFileFormat.storeFile = (content) => {
|
|
770
|
+
//logic to store file, e.g. put it to AWS S3
|
|
771
|
+
return '<url-to-download>';
|
|
772
|
+
};
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
### Custom MT
|
|
776
|
+
|
|
777
|
+
Example of [custom mt module](https://support.crowdin.com/crowdin-apps-modules/#custom-mt-machine-translation-module).
|
|
778
|
+
|
|
779
|
+
```javascript
|
|
780
|
+
const crowdinModule = require('@crowdin/app-project-module');
|
|
781
|
+
|
|
782
|
+
const configuration = {
|
|
783
|
+
baseUrl: 'https://123.ngrok.io',
|
|
784
|
+
clientId: 'clientId',
|
|
785
|
+
clientSecret: 'clientSecret',
|
|
786
|
+
name: 'Sample App',
|
|
787
|
+
identifier: 'sample-app',
|
|
788
|
+
description: 'Sample App description',
|
|
789
|
+
dbFolder: __dirname,
|
|
790
|
+
imagePath: __dirname + '/' + 'logo.png',
|
|
791
|
+
customMT: {
|
|
792
|
+
translate: async (client, context, projectId, source, target, strings) => {
|
|
793
|
+
//translate strings
|
|
794
|
+
const translations = ['hello', 'world'];
|
|
795
|
+
if (source === 'fr') {
|
|
796
|
+
throw 'Source language is not supported by the model';
|
|
797
|
+
}
|
|
798
|
+
return translations;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
crowdinModule.createApp(configuration);
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
### Profile Resources Menu
|
|
807
|
+
|
|
808
|
+
Example of [resources module](https://support.crowdin.com/crowdin-apps-modules/#resources-module).
|
|
809
|
+
|
|
810
|
+
```javascript
|
|
811
|
+
const crowdinModule = require('@crowdin/app-project-module');
|
|
812
|
+
|
|
813
|
+
const configuration = {
|
|
814
|
+
baseUrl: 'https://123.ngrok.io',
|
|
815
|
+
clientId: 'clientId',
|
|
816
|
+
clientSecret: 'clientSecret',
|
|
817
|
+
name: 'Sample App',
|
|
818
|
+
identifier: 'sample-app',
|
|
819
|
+
description: 'Sample App description',
|
|
820
|
+
dbFolder: __dirname,
|
|
821
|
+
imagePath: __dirname + '/' + 'logo.png',
|
|
822
|
+
profileResourcesMenu: {
|
|
823
|
+
fileName: 'setup.html',
|
|
824
|
+
uiPath: __dirname + '/' + 'public',
|
|
825
|
+
environments: 'crowdin-enterprise',
|
|
826
|
+
},
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
crowdinModule.createApp(configuration);
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
The `profileResourcesMenu` module can work as an extension to other modules be something like a configuration UI for them.
|
|
833
|
+
|
|
834
|
+
```javascript
|
|
835
|
+
const crowdinModule = require('@crowdin/app-project-module');
|
|
836
|
+
const express = require('express');
|
|
837
|
+
|
|
838
|
+
const app = express();
|
|
839
|
+
|
|
840
|
+
const configuration = {
|
|
841
|
+
baseUrl: 'https://123.ngrok.io',
|
|
842
|
+
clientId: 'clientId',
|
|
843
|
+
clientSecret: 'clientSecret',
|
|
844
|
+
name: 'Sample App',
|
|
845
|
+
identifier: 'sample-app',
|
|
846
|
+
description: 'Sample App description',
|
|
847
|
+
dbFolder: __dirname,
|
|
848
|
+
imagePath: __dirname + '/' + 'logo.png',
|
|
849
|
+
profileResourcesMenu: {
|
|
850
|
+
fileName: 'setup.html',
|
|
851
|
+
uiPath: __dirname + '/' + 'public',
|
|
852
|
+
environments: 'crowdin',
|
|
853
|
+
},
|
|
854
|
+
customMT: {
|
|
855
|
+
translate,
|
|
856
|
+
},
|
|
857
|
+
onUninstall: cleanup
|
|
858
|
+
};
|
|
859
|
+
|
|
860
|
+
const crowdinApp = crowdinModule.addCrowdinEndpoints(app, configuration);
|
|
861
|
+
|
|
862
|
+
async function cleanup(organization, allCredentials) {
|
|
863
|
+
//Cleanup logic
|
|
864
|
+
await crowdinApp.deleteMetadata(organization);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
async function translate(crowdinClient, context, projectId, source, target, strings) {
|
|
868
|
+
const organization = context.jwtPayload.domain || context.jwtPayload.context.organization_id;
|
|
869
|
+
const metadata = await crowdinApp.getMetadata(organization);
|
|
870
|
+
//do translation based on metadata
|
|
871
|
+
const translations = ['hello', 'world'];
|
|
872
|
+
return translations;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
//extra endpoints for resources UI
|
|
876
|
+
app.post('/metadata', async (req, res) => {
|
|
877
|
+
const { context } = await crowdinApp.establishCrowdinConnection(req.query.jwt);
|
|
878
|
+
const organization = context.jwtPayload.domain || context.jwtPayload.context.organization_id;
|
|
879
|
+
const metadata = await crowdinApp.getMetadata(organization);
|
|
880
|
+
await crowdinApp.saveMetadata(organization, req.body);
|
|
881
|
+
res.status(204).end();
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
app.get('/metadata', async (req, res) => {
|
|
885
|
+
const { context } = await crowdinApp.establishCrowdinConnection(req.query.jwt);
|
|
886
|
+
const organization = context.jwtPayload.domain || context.jwtPayload.context.organization_id;
|
|
887
|
+
const metadata = await crowdinApp.getMetadata(organization) || {};
|
|
888
|
+
res.status(200).send(metadata);
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
app.listen(3000, () => console.log('Crowdin app started'));
|
|
892
|
+
```
|
|
893
|
+
|
|
894
|
+
### Other modules
|
|
895
|
+
|
|
896
|
+
Framework also supports following modules:
|
|
897
|
+
|
|
898
|
+
- [organization-menu module](https://support.crowdin.com/enterprise/crowdin-apps-modules/#organization-menu-module)
|
|
899
|
+
- [editor-right-panel module](https://support.crowdin.com/crowdin-apps-modules/#editor-panels-module)
|
|
900
|
+
- [project-menu module](https://support.crowdin.com/crowdin-apps-modules/#project-menu-module)
|
|
901
|
+
- [project-menu-crowdsource module](https://developer.crowdin.com/crowdin-apps-module-project-menu-crowdsource/)
|
|
902
|
+
- [project-tools module](https://support.crowdin.com/crowdin-apps-modules/#tools-module)
|
|
903
|
+
- [project-reports module](https://support.crowdin.com/crowdin-apps-modules/#reports-module)
|
|
904
|
+
|
|
905
|
+
Example of Project Reports Module:
|
|
906
|
+
|
|
907
|
+
```javascript
|
|
908
|
+
const crowdinModule = require('@crowdin/app-project-module');
|
|
909
|
+
|
|
910
|
+
const configuration = {
|
|
911
|
+
baseUrl: 'https://123.ngrok.io',
|
|
912
|
+
clientId: 'clientId',
|
|
913
|
+
clientSecret: 'clientSecret',
|
|
914
|
+
name: 'Sample App',
|
|
915
|
+
identifier: 'sample-app',
|
|
916
|
+
description: 'Sample App description',
|
|
917
|
+
dbFolder: __dirname,
|
|
918
|
+
imagePath: __dirname + '/' + 'logo.png',
|
|
919
|
+
projectReports: { //can be editorRightPanel, projectMenu, projectTools, projectMenuCrowdsource
|
|
920
|
+
imagePath: __dirname + '/' + 'reports.png',
|
|
921
|
+
fileName: 'reports.html', //optional, only needed if file is not index.html
|
|
922
|
+
uiPath: __dirname + '/' + 'public', // folder where UI of the module is located (js, html, css files)
|
|
923
|
+
allowUnauthorized: true //make module publicly available without crowdin context
|
|
924
|
+
},
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
crowdinModule.createApp(configuration);
|
|
928
|
+
```
|
|
929
|
+
|
|
930
|
+
## Other options
|
|
931
|
+
|
|
932
|
+
### Options for Crowdin JWT token validation
|
|
933
|
+
|
|
934
|
+
```js
|
|
935
|
+
configuration.jwtValidationOptions = {
|
|
936
|
+
ignoreExpiration: false, //ignore check if jwt is expired or not
|
|
937
|
+
};
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
### App authentication type
|
|
941
|
+
|
|
942
|
+
```js
|
|
943
|
+
configuration.authenticationType = 'authorization_code'; //default is "crowdin_app"
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
### Disable global error handling
|
|
947
|
+
|
|
948
|
+
This module will handle all `unhandledRejection` and `uncaughtException` errors, log them and not kill the Node process.
|
|
949
|
+
Usually this means that code was not properly designed and contains unsafe places. And not always this built in behaviour will be suitable.
|
|
950
|
+
Therefore you can disable it:
|
|
951
|
+
|
|
952
|
+
```js
|
|
953
|
+
configuration.disableGlobalErrorHandling = true;
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
### Use React JSON Schema forms as module front-end
|
|
957
|
+
|
|
958
|
+
For all modules that have UI (except of `projectIntegration` module) you can use React JSON Schema forms ([docs](/https://github.com/rjsf-team/react-jsonschema-form)) as front-end.
|
|
959
|
+
To achieve this you can specify property `formSchema` in module definition.
|
|
960
|
+
```js
|
|
961
|
+
configuration.projectMenu = {
|
|
962
|
+
formSchema: {
|
|
963
|
+
"title": "A registration form",
|
|
964
|
+
"description": "A simple form example.",
|
|
965
|
+
"type": "object",
|
|
966
|
+
"required": [
|
|
967
|
+
"firstName",
|
|
968
|
+
"lastName"
|
|
969
|
+
],
|
|
970
|
+
"properties": {
|
|
971
|
+
"firstName": {
|
|
972
|
+
"type": "string",
|
|
973
|
+
"title": "First name",
|
|
974
|
+
"default": "Chuck"
|
|
975
|
+
},
|
|
976
|
+
"lastName": {
|
|
977
|
+
"type": "string",
|
|
978
|
+
"title": "Last name"
|
|
979
|
+
},
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
```
|
|
984
|
+
By default, form data will be stored in app metadata. To retrieve data you can use this code:
|
|
985
|
+
```js
|
|
986
|
+
crowdinApp.getMetadata(`${organizationId}-${projectId}`);
|
|
987
|
+
```
|
|
988
|
+
To customize this behavior you can provide `formDataUrl` property that will contain URL to custom action in your app. This action should support both GET request for fetching data and POST request to save new data.
|
|
6
989
|
|
|
7
990
|
## Contributing
|
|
8
991
|
|