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