@crowdin/app-project-module 0.26.6 → 0.28.0-10

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.
Files changed (37) hide show
  1. package/CONTRIBUTING.md +19 -1
  2. package/README.md +1 -855
  3. package/out/handlers/crowdin-file-progress.d.ts +2 -2
  4. package/out/handlers/crowdin-file-progress.js +9 -4
  5. package/out/handlers/crowdin-update.js +4 -3
  6. package/out/handlers/crowdin-webhook.d.ts +4 -0
  7. package/out/handlers/crowdin-webhook.js +43 -0
  8. package/out/handlers/form-data-display.d.ts +3 -0
  9. package/out/handlers/form-data-display.js +46 -0
  10. package/out/handlers/form-data-save.d.ts +3 -0
  11. package/out/handlers/form-data-save.js +56 -0
  12. package/out/handlers/integration-logout.js +4 -0
  13. package/out/handlers/integration-webhook.d.ts +4 -0
  14. package/out/handlers/integration-webhook.js +39 -0
  15. package/out/handlers/main.js +7 -1
  16. package/out/handlers/settings-save.d.ts +2 -2
  17. package/out/handlers/settings-save.js +8 -3
  18. package/out/handlers/uninstall.js +4 -0
  19. package/out/index.js +50 -10
  20. package/out/middlewares/render-ui-module.d.ts +4 -0
  21. package/out/middlewares/render-ui-module.js +33 -0
  22. package/out/models/index.d.ts +79 -7
  23. package/out/models/index.js +13 -1
  24. package/out/static/css/styles.css +96 -0
  25. package/out/static/js/dependent.js +307 -0
  26. package/out/static/js/form.js +115 -0
  27. package/out/static/js/main.js +11 -1
  28. package/out/util/cron.js +9 -10
  29. package/out/util/defaults.js +55 -12
  30. package/out/util/index.js +5 -1
  31. package/out/util/webhooks.d.ts +29 -0
  32. package/out/util/webhooks.js +308 -0
  33. package/out/views/form.handlebars +29 -0
  34. package/out/views/login.handlebars +84 -16
  35. package/out/views/main.handlebars +171 -88
  36. package/out/views/partials/head.handlebars +5 -4
  37. package/package.json +37 -23
package/README.md CHANGED
@@ -2,861 +2,7 @@
2
2
 
3
3
  Module that will automatically add all necessary endpoints for Crowdin App.
4
4
 
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
- integrationPagination: true, //enable pagination on integration files list
93
- integrationOneLevelFetching: true, //turn on request when opening a directory and pass its id
94
- integrationSearchListener: true, //turn on search listener and pass search string
95
- getIntegrationFiles: async (credentials, appSettings, parentId, search, page) => {
96
- //here you need to fetch files/objects from integration
97
- const res = [
98
- {
99
- id: '12',
100
- name: 'File from integration',
101
- type: 'json',
102
- parentId: '10'
103
- },
104
- {
105
- id: '14',
106
- name: 'File from integration2',
107
- type: 'xml',
108
- parentId: '11'
109
- },
110
- {
111
- id: '10',
112
- name: 'Folder from integration'
113
- },
114
- {
115
- id: '11',
116
- name: 'Folder from integratio2'
117
- customContent: `<div style='color: red;'>Custom Folder name</div>`,
118
- parentId: '1'
119
- },
120
- {
121
- id: '1',
122
- name: 'Branch from integratio',
123
- nodeType: '2'
124
- },
125
- ];
126
- return {
127
- data: res,
128
- message: 'Test',
129
- stopPagination: true, //send if no next files page
130
- };
131
- },
132
- updateCrowdin: async (projectId, client, credentials, request, rootFolder, appSettings) => {
133
- //here you need to get data from integration and upload it to Crowdin
134
- console.log(`Request for updating data in Crowdin ${JSON.stringify(request)}`);
135
- const directories = await client.sourceFilesApi
136
- .withFetchAll()
137
- .listProjectDirectories(projectId);
138
- const { folder, files } = await crowdinAppFunctions.getOrCreateFolder({
139
- directories: directories.data.map((d) => d.data),
140
- client,
141
- projectId,
142
- directoryName: 'Folder from integration',
143
- parentDirectory: rootFolder
144
- });
145
- const fileContent = {
146
- title: 'Hello World',
147
- };
148
- await crowdinAppFunctions.updateOrCreateFile({
149
- client,
150
- projectId,
151
- name: 'integration.json',
152
- title: 'Sample file from integration',
153
- type: 'json',
154
- directoryId: folder.id,
155
- data: fileContent,
156
- file: files.find((f) => f.name === 'integration.json'),
157
- });
158
- return {
159
- message: 'Some message',
160
- };
161
- },
162
- updateIntegration: async (projectId, client, credentials, request, rootFolder, appSettings) => {
163
- //here should be logic to get translations from Crowdin and upload them to integration
164
- console.log(`Request for updating data in Integration ${JSON.stringify(request)}`);
165
- const directories = await client.sourceFilesApi
166
- .withFetchAll()
167
- .listProjectDirectories(projectId);
168
- const { files } = await crowdinAppFunctions.getFolder({
169
- directories: directories.data.map((d) => d.data),
170
- client,
171
- projectId,
172
- directoryName: 'Folder from integration',
173
- parentDirectory: rootFolder
174
- });
175
- const file = files.find((f) => f.name === 'integration.json');
176
- if (file) {
177
- const translationsLink =
178
- await client.translationsApi.buildProjectFileTranslation(
179
- projectId,
180
- file.id,
181
- { targetLanguageId: 'uk' },
182
- );
183
- if (!translationsLink) {
184
- return;
185
- }
186
- const response = await axios.get(translationsLink.data.url);
187
- console.log(response.data);
188
- }
189
- },
190
- onLogout: async (projectId, client, credentials, appSettings) => {
191
- //cleanup logic
192
- }
193
- }
194
- };
195
-
196
- crowdinModule.createApp(configuration);
197
- ```
198
-
199
- ## Payment
200
-
201
- By default App does not have any subscription and it's free to use. But you can override this.
202
-
203
- ```javascript
204
- configuration.pricing = {
205
- plantType: 'recurring',
206
- trial: 14, //amount of days to use app for free
207
- trialCrowdin: 14, //amount of days specifically in crowdin workspace
208
- trialEnterprise: 30, //amount of days specifically for enterprise
209
- cachingSeconds: 12 * 60 * 60, //time in seconds of how long to cache subscription info
210
- infoDisplayDaysThreshold: 14 //number of days threshold to check if subscription info should be displayed (if not defined then info will be always visible)
211
- };
212
- ```
213
-
214
- ## Authorization
215
-
216
- ### Customize your app login form
217
-
218
- By default, login page for your app will require only to enter `apiToken` to communicate with third party service.
219
- But there is also a possibility to customize it.
220
-
221
- ```javascript
222
- configuration.projectIntegration.loginForm = {
223
- fields: [
224
- {
225
- key: 'username',
226
- label: 'Username',
227
- },
228
- {
229
- key: 'password',
230
- label: 'Password',
231
- type: 'password'
232
- },
233
- {
234
- label: 'Api creds',
235
- },
236
- {
237
- helpText: 'Api Key for http requests',
238
- key: 'apiKey',
239
- label: 'Api Key'
240
- },
241
- {
242
- key: 'server',
243
- label: 'Data center',
244
- type: 'select',
245
- defaultValue: '1',
246
- options: [
247
- {
248
- value: '1',
249
- label: 'USA'
250
- },
251
- {
252
- value: '2',
253
- label: 'EU'
254
- }
255
- ]
256
- }
257
- ]
258
- };
259
- ```
260
-
261
- ### OAuth2 support
262
-
263
- In case if third party service uses OAuth2 for authorization use `oauthLogin` field to configure it.
264
- `loginForm` **in this case should remain undefined**.
265
-
266
- Github example:
267
-
268
- ```javascript
269
- configuration.projectIntegration.oauthLogin = {
270
- authorizationUrl: 'https://github.com/login/oauth/authorize',
271
- clientId: 'github_app_client_id',
272
- clientSecret: 'github_app_client_secret',
273
- accessTokenUrl: 'https://github.com/login/oauth/access_token'
274
- }
275
- ```
276
-
277
- Google example:
278
-
279
- ```javascript
280
- configuration.projectIntegration.oauthLogin = {
281
- scope: 'https%3A//www.googleapis.com/auth/userinfo.email',
282
- authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
283
- clientId: 'google_web_app_client_id',
284
- clientSecret: 'google_web_app_client_secret',
285
- accessTokenUrl: 'https://oauth2.googleapis.com/token',
286
- extraAutorizationUrlParameters: {
287
- response_type: 'code',
288
- access_type: 'offline',
289
- prompt: 'consent'
290
- },
291
- extraAccessTokenParameters: {
292
- grant_type: 'authorization_code'
293
- },
294
- extraRefreshTokenParameters: {
295
- grant_type: 'refresh_token'
296
- },
297
- refresh: true
298
- }
299
- ```
300
-
301
- The `oauthLogin` property allows you to customize many different properties, mappings, etc. So that you can integrate with any OAuth2 implementation.
302
- Main default values:
303
-
304
- - redirect uri prefix will be `/oauth/code`
305
- - client id field name in url parameters and in request payload will be `client_id`
306
- - client secret field name in request payload will be `client_secret`
307
- - access token field name should be `access_token`
308
- - 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
309
- - access token field name should be `refresh_token`
310
- - expires in field name should be `expires_in` (value should be in seconds)
311
-
312
- This module rely that OAuth2 protocol is implemented by third party service in this way:
313
-
314
- - 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)
315
- - 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)
316
- - both requests will return JSON response with body that contains `accessToken` and, if enabled, `refreshToken` (optional) and `expireIn`
317
-
318
- 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).
319
-
320
- Mailup example:
321
-
322
- ```javascript
323
- const clientId = 'client_id';
324
- const clientSecret = 'client_secret';
325
- const tokenUrl = 'https://services.mailup.com/Authorization/OAuth/Token';
326
-
327
- configuration.projectIntegration.oauthLogin = {
328
- authorizationUrl: 'https://services.mailup.com/Authorization/OAuth/LogOn',
329
- clientId,
330
- clientSecret,
331
- extraAutorizationUrlParameters: {
332
- response_type: 'code'
333
- },
334
- refresh: true,
335
- performGetTokenRequest: async (code, query, url) => {
336
- //query is an object with all query params
337
- //url is an url string that OAuth server used to call us back
338
- const url = `${tokenUrl}?code=${code}&grant_type=authorization_code`;
339
- const headers = {
340
- 'Authorization': `Bearer ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`
341
- };
342
- return (await axios.get(url, { headers })).data;
343
- },
344
- performRefreshTokenRequest: async (credentials) => {
345
- const params = {
346
- refresh_token: credentials.refreshToken,
347
- grant_type: 'refresh_token',
348
- client_id: clientId,
349
- client_secret: clientSecret
350
- };
351
- const data = Object.keys(params)
352
- .map((key) => `${key}=${encodeURIComponent(params[key])}`)
353
- .join('&');
354
- const headers = {
355
- 'Content-Type': 'application/x-www-form-urlencoded'
356
- };
357
- return (await axios.post(tokenUrl, data, { headers })).data;
358
- }
359
- }
360
- ```
361
-
362
- In addition you can specify extra fields on login screen that you can use to dynamically build authorization url.
363
-
364
- ```javascript
365
- configuration.projectIntegration.oauthLogin.loginFields = [
366
- {
367
- key: 'region',
368
- label: 'Region',
369
- type: 'select',
370
- defaultValue: 'NA',
371
- options: [
372
- {
373
- value: 'NA',
374
- label: 'North American'
375
- },
376
- {
377
- value: 'EU',
378
- label: 'Europe'
379
- },
380
- {
381
- value: 'AZURE_NA',
382
- label: 'Azure North American'
383
- }
384
- ]
385
- }
386
- ];
387
-
388
- configuration.projectIntegration.oauthLogin.getAuthorizationUrl = (redirectUrl, loginForm) => {
389
- const region = loginForm.region;
390
- if (region === 'EU') {
391
- return `https://eu-region.com?client_id=<client-id>&redirect_uri=${redirectUrl}`;
392
- } else {
393
- return `https://default-region.com?client_id=<client-id>&redirect_uri=${redirectUrl}`;
394
- }
395
- };
396
- ```
397
-
398
- Please refer to jsdoc for more details.
399
-
400
- ## Storage
401
-
402
- Module can be configured to use following storages:
403
-
404
- ### SQLite
405
-
406
- ```javascript
407
- //specify folder where sqlite db file will be located
408
- configuration.dbFolder = __dirname;
409
- ```
410
-
411
- ### PostgreSQL
412
-
413
- ```javascript
414
- configuration.postgreConfig = {
415
- host: 'localhost',
416
- user: 'postgres',
417
- password: 'password',
418
- database: 'test'
419
- };
420
- ```
421
-
422
- ### MySQL
423
-
424
- ```javascript
425
- configuration.mysqlConfig = {
426
- host: 'localhost',
427
- user: 'root',
428
- password: 'password',
429
- database: 'test'
430
- };
431
- ```
432
-
433
- ## Settings window
434
-
435
- It is also possible to define settings window for your app where users can customize integration flow.
436
-
437
- ```javascript
438
- configuration.projectIntegration.getConfiguration = (projectId, crowdinClient, integrationCredentials) => {
439
- return [
440
- {
441
- label: 'GENERAL'
442
- },
443
- {
444
- key: 'flag',
445
- label: 'Checkbox',
446
- type: 'checkbox'
447
- },
448
- {
449
- key: 'text',
450
- label: 'Text input',
451
- type: 'text',
452
- helpText: 'Help text'
453
- },
454
- {
455
- key: 'option',
456
- label: 'Select',
457
- type: 'select',
458
- defaultValue: '12',
459
- isSearchable: true, //allow to search for option(s), default is `false`
460
- isMulti: true, //allow to select multiple options, default is `false`
461
- options: [
462
- {
463
- value: '12',
464
- label: 'Option'
465
- }
466
- ]
467
- }
468
- ]
469
- }
470
-
471
- //auto reload on settings updates
472
- configuration.projectIntegration.reloadOnConfigSave = true;
473
- ```
474
-
475
- ## Info window
476
-
477
- You also can define section with some information notes or help section for your app.
478
-
479
- ```javascript
480
- configuration.projectIntegration.infoModal = {
481
- title: 'Info',
482
- content: `
483
- <h1>This is your app help section</h1>
484
- </br>
485
- <h2>This is just an example</h2>
486
- `
487
- }
488
- ```
489
-
490
- ## Background tasks
491
-
492
- In order to register background tasks that app will invoke periodically invoke you can use `cronJobs` field.
493
-
494
- ```javascript
495
- configuration.projectIntegration.cronJobs = [
496
- {
497
- //every 10 seconds
498
- expression: '*/10 * * * * *',
499
- task: (projectId, client, apiCredentials, appRootFolder, config) => {
500
- console.log(`Running background task for project : ${projectId}`);
501
- console.log(`Api credentials : ${JSON.stringify(apiCredentials)}`);
502
- console.log(`App config : ${JSON.stringify(config)}`);
503
- console.log(appRootFolder ? JSON.stringify(appRootFolder) : 'No root folder');
504
- }
505
- }
506
- ]
507
- ```
508
-
509
- For cron syntax guide please refer to this [documentation](https://github.com/node-cron/node-cron#cron-syntax).
510
-
511
- ## Errors Handling
512
-
513
- ### Error propagation
514
-
515
- 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.
516
- e.g. check if entered credentials are valid:
517
-
518
- ```javascript
519
- configuration.projectIntegration.checkConnection = (credentials) => {
520
- if (!credentials.password || credentials.password.length < 6) {
521
- throw 'Password is too weak';
522
- }
523
- //or call an service API with those credentials and check if request will be successful
524
- };
525
- ```
526
-
527
- 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.
528
- e.g. when your service has some specific session duration timeout or extra conditions which are not covered by this framework
529
-
530
- ```javascript
531
- configuration.projectIntegration.getIntegrationFiles = async (credentials, appSettings) => {
532
- //do a request/custom logic here
533
- const sessionStillValid = false;
534
- if (!sessionStillValid) {
535
- throw {
536
- message: 'session expired',
537
- code: 401
538
- }
539
- }
540
- //business logic
541
- }
542
- ```
543
-
544
- ### Error interceptor
545
-
546
- You can also provide interceptor to catch errors and process them (e.g. to log them in the centralized place).
547
-
548
- ```javascript
549
- const Sentry = require('@sentry/node');
550
-
551
- Sentry.init({
552
- dsn: "https://examplePublicKey@o0.ingest.sentry.io/0",
553
- tracesSampleRate: 1.0,
554
- });
555
-
556
- configuration.onError = (error) => {
557
- Sentry.captureException(e);
558
- };
559
- ```
560
-
561
- ### Debug mode
562
-
563
- Also you can turn on the debug mode and application will log everything (useful of debugging).
564
-
565
- ```javascript
566
- configuration.logger = {
567
- enabled: true
568
- };
569
- ```
570
-
571
- Or even you can pass you own function to log messages (e.g. send them to external monitoring system).
572
-
573
- ```javascript
574
- configuration.logger = {
575
- enabled: true,
576
- log: (message) => {
577
- console.log(message);
578
- }
579
- };
580
- ```
581
-
582
- ## Modules
583
-
584
- ### Custom File Format
585
-
586
- Example of [custom file format module](https://support.crowdin.com/crowdin-apps-modules/#custom-file-format-module).
587
-
588
- ```javascript
589
- const crowdinModule = require('@crowdin/app-project-module');
590
- const convert = require('xml-js');
591
-
592
- const configuration = {
593
- baseUrl: 'https://123.ngrok.io',
594
- clientId: 'clientId',
595
- clientSecret: 'clientSecret',
596
- name: 'Sample App',
597
- identifier: 'sample-app',
598
- description: 'Sample App description',
599
- dbFolder: __dirname,
600
- imagePath: __dirname + '/' + 'logo.png',
601
- customFileFormat: {
602
- filesFolder: __dirname,
603
- type: 'type-xyz',
604
- multilingual: false,
605
- autoUploadTranslations: true, //useful when single language format
606
- signaturePatterns: {
607
- fileName: '^.+\.xml$'
608
- },
609
- customSrxSupported: true,
610
- parseFile: async (file, req, client, context, projectId) => {
611
- const xml = convert.xml2json(file, { compact: true, spaces: 4 });
612
- const fileContent = JSON.parse(xml);
613
- //parse logic
614
- const strings = [];
615
- return { strings, error: 'Some error message' };
616
- },
617
- buildFile: async (file, req, strings, client, context, projectId) => {
618
- const xml = convert.xml2json(file, { compact: true, spaces: 4 });
619
- const fileContent = JSON.parse(xml);
620
- //build logic
621
- const contentFile = convert.json2xml(
622
- fileContent,
623
- {
624
- compact: true,
625
- ignoreComment: false,
626
- spaces: 4
627
- }
628
- );
629
- return { contentFile }
630
- }
631
- }
632
- };
633
-
634
- crowdinModule.createApp(configuration);
635
- ```
636
-
637
- Also custom file format module can support strings export.
638
-
639
- ```javascript
640
- const crowdinModule = require('@crowdin/app-project-module');
641
- const convert = require('xml-js');
642
-
643
- const configuration = {
644
- baseUrl: 'https://123.ngrok.io',
645
- clientId: 'clientId',
646
- clientSecret: 'clientSecret',
647
- name: 'Sample App',
648
- identifier: 'sample-app',
649
- description: 'Sample App description',
650
- dbFolder: __dirname,
651
- imagePath: __dirname + '/' + 'logo.png',
652
- customFileFormat: {
653
- filesFolder: __dirname,
654
- type: 'type-xyz',
655
- stringsExport: true,
656
- multilingualExport: true,
657
- extensions: [
658
- '.resx'
659
- ],
660
- exportStrings: async (req, strings, client, context, projectId) => {
661
- const file = req.file;
662
- //export logic
663
- return { contentFile: '' }
664
- }
665
- }
666
- };
667
-
668
- crowdinModule.createApp(configuration);
669
- ```
670
-
671
- By default custom file format will use `filesFolder` as a temporary folder to store huge responses. But it can be overridden:
672
-
673
- ```javascript
674
- configuration.customFileFormat.storeFile = (content) => {
675
- //logic to store file, e.g. put it to AWS S3
676
- return '<url-to-download>';
677
- };
678
- ```
679
-
680
- ### Custom MT
681
-
682
- Example of [custom mt module](https://support.crowdin.com/crowdin-apps-modules/#custom-mt-machine-translation-module).
683
-
684
- ```javascript
685
- const crowdinModule = require('@crowdin/app-project-module');
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
- customMT: {
697
- translate: async (client, context, projectId, source, target, strings) => {
698
- //translate strings
699
- const translations = ['hello', 'world'];
700
- if (source === 'fr') {
701
- throw 'Source language is not supported by the model';
702
- }
703
- return translations;
704
- }
705
- }
706
- };
707
-
708
- crowdinModule.createApp(configuration);
709
- ```
710
-
711
- ### Profile Resources Menu
712
-
713
- Example of [resources module](https://support.crowdin.com/crowdin-apps-modules/#resources-module).
714
-
715
- ```javascript
716
- const crowdinModule = require('@crowdin/app-project-module');
717
-
718
- const configuration = {
719
- baseUrl: 'https://123.ngrok.io',
720
- clientId: 'clientId',
721
- clientSecret: 'clientSecret',
722
- name: 'Sample App',
723
- identifier: 'sample-app',
724
- description: 'Sample App description',
725
- dbFolder: __dirname,
726
- imagePath: __dirname + '/' + 'logo.png',
727
- profileResourcesMenu: {
728
- fileName: 'setup.html',
729
- uiPath: __dirname + '/' + 'public',
730
- environments: 'crowdin-enterprise',
731
- },
732
- };
733
-
734
- crowdinModule.createApp(configuration);
735
- ```
736
-
737
- The `profileResourcesMenu` module can work as an extension to other modules be something like a configuration UI for them.
738
-
739
- ```javascript
740
- const crowdinModule = require('@crowdin/app-project-module');
741
- const express = require('express');
742
-
743
- const app = express();
744
-
745
- const configuration = {
746
- baseUrl: 'https://123.ngrok.io',
747
- clientId: 'clientId',
748
- clientSecret: 'clientSecret',
749
- name: 'Sample App',
750
- identifier: 'sample-app',
751
- description: 'Sample App description',
752
- dbFolder: __dirname,
753
- imagePath: __dirname + '/' + 'logo.png',
754
- profileResourcesMenu: {
755
- fileName: 'setup.html',
756
- uiPath: __dirname + '/' + 'public',
757
- environments: 'crowdin',
758
- },
759
- customMT: {
760
- translate,
761
- },
762
- onUninstall: cleanup
763
- };
764
-
765
- const crowdinApp = crowdinModule.addCrowdinEndpoints(app, configuration);
766
-
767
- async function cleanup(organization, allCredentials) {
768
- //Cleanup logic
769
- await crowdinApp.deleteMetadata(organization);
770
- }
771
-
772
- async function translate(crowdinClient, context, projectId, source, target, strings) {
773
- const organization = context.jwtPayload.domain || context.jwtPayload.context.organization_id;
774
- const metadata = await crowdinApp.getMetadata(organization);
775
- //do translation based on metadata
776
- const translations = ['hello', 'world'];
777
- return translations;
778
- }
779
-
780
- //extra endpoints for resources UI
781
- app.post('/metadata', async (req, res) => {
782
- const { context } = await crowdinApp.establishCrowdinConnection(req.query.jwt);
783
- const organization = context.jwtPayload.domain || context.jwtPayload.context.organization_id;
784
- const metadata = await crowdinApp.getMetadata(organization);
785
- await crowdinApp.saveMetadata(organization, req.body);
786
- res.status(204).end();
787
- });
788
-
789
- app.get('/metadata', async (req, res) => {
790
- const { context } = await crowdinApp.establishCrowdinConnection(req.query.jwt);
791
- const organization = context.jwtPayload.domain || context.jwtPayload.context.organization_id;
792
- const metadata = await crowdinApp.getMetadata(organization) || {};
793
- res.status(200).send(metadata);
794
- });
795
-
796
- app.listen(3000, () => console.log('Crowdin app started'));
797
- ```
798
-
799
- ### Other modules
800
-
801
- Framework also supports following modules:
802
-
803
- - [organization-menu module](https://support.crowdin.com/enterprise/crowdin-apps-modules/#organization-menu-module)
804
- - [editor-right-panel module](https://support.crowdin.com/crowdin-apps-modules/#editor-panels-module)
805
- - [project-menu module](https://support.crowdin.com/crowdin-apps-modules/#project-menu-module)
806
- - [project-menu-crowdsource module](https://developer.crowdin.com/crowdin-apps-module-project-menu-crowdsource/)
807
- - [project-tools module](https://support.crowdin.com/crowdin-apps-modules/#tools-module)
808
- - [project-reports module](https://support.crowdin.com/crowdin-apps-modules/#reports-module)
809
-
810
- Example of Project Reports Module:
811
-
812
- ```javascript
813
- const crowdinModule = require('@crowdin/app-project-module');
814
-
815
- const configuration = {
816
- baseUrl: 'https://123.ngrok.io',
817
- clientId: 'clientId',
818
- clientSecret: 'clientSecret',
819
- name: 'Sample App',
820
- identifier: 'sample-app',
821
- description: 'Sample App description',
822
- dbFolder: __dirname,
823
- imagePath: __dirname + '/' + 'logo.png',
824
- projectReports: { //can be editorRightPanel, projectMenu, projectTools, projectMenuCrowdsource
825
- imagePath: __dirname + '/' + 'reports.png',
826
- fileName: 'reports.html', //optional, only needed if file is not index.html
827
- uiPath: __dirname + '/' + 'public', // folder where UI of the module is located (js, html, css files)
828
- allowUnauthorized: true //make module publicly available without crowdin context
829
- },
830
- };
831
-
832
- crowdinModule.createApp(configuration);
833
- ```
834
-
835
- ## Other options
836
-
837
- ### Options for Crowdin JWT token validation
838
-
839
- ```js
840
- configuration.jwtValidationOptions = {
841
- ignoreExpiration: false, //ignore check if jwt is expired or not
842
- };
843
- ```
844
-
845
- ### App authentication type
846
-
847
- ```js
848
- configuration.authenticationType = 'authorization_code'; //default is "crowdin_app"
849
- ```
850
-
851
- ### Disable global error handling
852
-
853
- This module will handle all `unhandledRejection` and `uncaughtException` errors, log them and not kill the Node process.
854
- Usually this means that code was not properly designed and contains unsafe places. And not always this built in behaviour will be suitable.
855
- Therefore you can disable it:
856
-
857
- ```js
858
- configuration.disableGlobalErrorHandling = true;
859
- ```
5
+ :bookmark: See the [docs](/docs) for more information.
860
6
 
861
7
  ## Contributing
862
8