# Overview

Project integration is one of the most common application types. It allows you to create and add a new integration into your Crowdin project. You can find it in the integrations section of the project page. It is available to project members with Manager (or higher) permissions.

[Integrations Module](https://support.crowdin.com/developer/crowdin-apps-module-project-integrations/)

It uses the `crowdin-simple-integration` component provided by the [Crowdin UI Kit](https://crowdin-web-components.s3.amazonaws.com/index.html).

## Sample App

```js ins="projectIntegration" "getIntegrationFiles" "updateCrowdin" "updateIntegration" collapse={28-38,51-102, 114-140} title="index.js"
    import crowdinModule from '@crowdin/app-project-module';
    import * as crowdinAppFunctions from '@crowdin/app-project-module/functions/crowdin';
    import axios from 'axios';

    const configuration = {
      baseUrl: 'https://123.ngrok.io',
      clientId: 'clientId',
      clientSecret: 'clientSecret',
      port: 8080,
      scopes: [
        crowdinModule.Scope.PROJECTS
      ],
      name: 'Sample App',
      identifier: 'sample-app',
      description: 'Sample App description',
      dbFolder: import.meta.dirname,
      imagePath: import.meta.dirname + '/logo.png',
      projectIntegration: {
        withRootFolder: true,
        excludedTargetLanguages: true,

        getIntegrationFiles: async ({
          credentials,
          client,
          projectId,
          settings,
          parentId,
          search,
          page,
          paginationData,
        }) => {
          const res = [
            {
              id: '12',
              name: 'File from integration',
              type: 'json',
              parentId: '10',
              createdAt: '2024-03-20T10:00:00Z',
              updatedAt: '2024-03-20T15:30:00Z'
            },
            {
              id: '14',
              name: 'File from integration 2',
              type: 'xml',
              parentId: '11',
              disabled: true,
              tooltip: 'Unsupported data type.',
              createdAt: '2024-03-20T10:00:00Z',
              updatedAt: '2024-03-20T15:30:00Z'
            }
          ];
          return {
            data: res
          };
        },

        updateCrowdin: async ({
          projectId,
          client,
          credentials,
          request,
          rootFolder,
          settings,
          uploadTranslations,
          job,
          excludedTargetLanguages,
        }) => {
          if (job.type !== 'cron') {
            await job.update({
              info: 'Preparing to upload'
            });
          }

          const directories = await client.sourceFilesApi
            .withFetchAll()
            .listProjectDirectories(projectId);

          const { folder, files } = await crowdinAppFunctions.getOrCreateFolder({
            directories: directories.data.map((d) => d.data),
            client,
            projectId,
            directoryName: 'Folder from integration',
            parentDirectory: rootFolder
          });

          const result = await job.update({
            progress: 5,
            info: 'Uploading files'
          });

          if (result?.isCanceled) {
            return;
          }

          const fileId = await crowdinAppFunctions.updateOrCreateFile({
            client,
            projectId,
            name: 'integration.json',
            title: 'Sample file from integration',
            type: 'json',
            directoryId: folder.id,
            data: '<file_content>',
            file: files.find((f) => f.name === 'integration.json'),
            excludedTargetLanguages,
          });

          await job.update({
            progress: 95
          });

          if (uploadTranslations) {
            await crowdinAppFunctions.uploadTranslations({
              client,
              projectId,
              fileId,
              language: 'es',
              fileName: 'integration.json',
              fileContent: { title: 'Hola Mundo' },
            });
          }
        },

        updateIntegration: async ({
          projectId,
          client,
          credentials,
          request,
          rootFolder,
          settings,
          job,
        }) => {
          const directories = await client.sourceFilesApi
            .withFetchAll()
            .listProjectDirectories(projectId);
          const { files } = await crowdinAppFunctions.getFolder({
            directories: directories.data.map((d) => d.data),
            client,
            projectId,
            directoryName: 'Folder from integration',
            parentDirectory: rootFolder
          });

          const file = files.find((f) => f.name === 'integration.json');
          if (file) {
            const link = await client.translationsApi.buildProjectFileTranslation(
              projectId,
              file.id,
              { targetLanguageId: 'uk' },
            );
            if (!link) {
              return;
            }
            const response = await axios.get(link.data.url);
          }
        },

        onLogout: async ({ projectId, client, credentials, settings }) => {
          // cleanup logic
        }
      }
    };

    crowdinModule.createApp(configuration);
    ```
  ```ts ins="projectIntegration" "getIntegrationFiles" "updateCrowdin" "updateIntegration" collapse={29-39,52-103, 115-141} title="index.ts"
    import crowdinModule from '@crowdin/app-project-module';
    import type { ClientConfig } from '@crowdin/app-project-module';
    import * as crowdinAppFunctions from '@crowdin/app-project-module/functions/crowdin';
    import axios from 'axios';

    const configuration: ClientConfig = {
      baseUrl: 'https://123.ngrok.io',
      clientId: 'clientId',
      clientSecret: 'clientSecret',
      port: 8080,
      scopes: [
        crowdinModule.Scope.PROJECTS
      ],
      name: 'Sample App',
      identifier: 'sample-app',
      description: 'Sample App description',
      dbFolder: import.meta.dirname,
      imagePath: import.meta.dirname + '/logo.png',
      projectIntegration: {
        withRootFolder: true,
        excludedTargetLanguages: true,

        getIntegrationFiles: async ({
          credentials,
          client,
          projectId,
          settings,
          parentId,
          search,
          page,
          paginationData,
        }) => {
          const res = [
            {
              id: '12',
              name: 'File from integration',
              type: 'json',
              parentId: '10',
              createdAt: '2024-03-20T10:00:00Z',
              updatedAt: '2024-03-20T15:30:00Z'
            },
            {
              id: '14',
              name: 'File from integration 2',
              type: 'xml',
              parentId: '11',
              disabled: true,
              tooltip: 'Unsupported data type.',
              createdAt: '2024-03-20T10:00:00Z',
              updatedAt: '2024-03-20T15:30:00Z'
            }
          ];
          return {
            data: res
          };
        },

        updateCrowdin: async ({
          projectId,
          client,
          credentials,
          request,
          rootFolder,
          settings,
          uploadTranslations,
          job,
          excludedTargetLanguages,
        }) => {
          if (String(job.type) !== 'cron') {
            await job.update({
              info: 'Preparing to upload'
            });
          }

          const directories = await client.sourceFilesApi
            .withFetchAll()
            .listProjectDirectories(projectId);

          const { folder, files } = await crowdinAppFunctions.getOrCreateFolder({
            directories: directories.data.map((d) => d.data),
            client,
            projectId,
            directoryName: 'Folder from integration',
            parentDirectory: rootFolder
          });

          const result = await job.update({
            progress: 5,
            info: 'Uploading files'
          });

          if (result?.isCanceled) {
            return;
          }

          const fileId = await crowdinAppFunctions.updateOrCreateFile({
            client,
            projectId,
            name: 'integration.json',
            title: 'Sample file from integration',
            type: 'json',
            directoryId: folder.id,
            data: '<file_content>',
            file: files.find((f) => f.name === 'integration.json'),
            excludedTargetLanguages,
          });

          await job.update({
            progress: 95
          });

          if (uploadTranslations) {
            await crowdinAppFunctions.uploadTranslations({
              client,
              projectId,
              fileId,
              language: 'es',
              fileName: 'integration.json',
              fileContent: { title: 'Hola Mundo' },
            });
          }
        },

        updateIntegration: async ({
          projectId,
          client,
          credentials,
          request,
          rootFolder,
          settings,
          job,
        }) => {
          const directories = await client.sourceFilesApi
            .withFetchAll()
            .listProjectDirectories(projectId);
          const { files } = await crowdinAppFunctions.getFolder({
            directories: directories.data.map((d) => d.data),
            client,
            projectId,
            directoryName: 'Folder from integration',
            parentDirectory: rootFolder
          });

          const file = files.find((f) => f.name === 'integration.json');
          if (file) {
            const link = await client.translationsApi.buildProjectFileTranslation(
              projectId,
              file.id,
              { targetLanguageId: 'uk' },
            );
            if (!link) {
              return;
            }
            const response = await axios.get(link.data.url);
          }
        },

        onLogout: async ({ projectId, client, credentials, settings }) => {
          // cleanup logic
        }
      }
    };

    crowdinModule.createApp(configuration);
    ```
  ## Configuration

| Parameter<div class="w-56"/>      | Description                                                                                                                                       | Default |
|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------|---------|
| `withRootFolder`                  | Whether to create a root integration folder in Crowdin.                                                                                           | `false` |
| `integrationPagination`           | Enable pagination on integration files list.                                                                                                      | `false` |
| `integrationOneLevelFetching`     | Turn on request when opening a directory and pass its id.                                                                                         | `false` |
| `integrationSearchListener`       | Turn on search listener and pass search string.                                                                                                   | `false` |
| `progressiveCrowdinFilesLoading`  | Enable progressive loading for Crowdin files. Directories load first, then files load separately. Improves UX for large projects.                 | `false` |
| `uploadTranslations`              | Enable the option to download translations from integration. When enabled, you need to handle it in the `updateCrowdin` function. | `false` |
| `excludedTargetLanguages`         | Enable the option to upload file for translation into selected languages.                                                                         | `false` |
| `forcePushTranslations`           | Enable force push button to allow pushing translations regardless of eTag. Only relevant when using `job.fetchTranslation`.                       | `false` |
| `forcePushSources`                | Enable force push button to allow pushing all source files to Crowdin regardless of whether they've been modified since the last sync.            | `false` |

### `getIntegrationFiles` Function

This function is used to retrieve files and folders from the integration.

##### Parameters

- `credentials` - Crowdin API credentials.
- `client` - Crowdin API client.
- `projectId` - Crowdin project ID.
- `settings` - Application settings.
- `parentId` - Parent folder identifier.
- `search` - Search string.
- `page` - Page number.
- `paginationData` - Custom pagination data object for dynamic pagination.

##### Return Value

Should retrieve files and folders from the integration and return them in the following format:

```yml
{
  data: [
    {
      id: string,
      name: string,
      type?: string,
      parentId?: string,
      nodeType?: string,
      labels?: [
        {
          text: string,
          type: string,
          color: string
        }
      ],
      customContent?: string,
      disabled?: boolean,
      tooltip?: string,
      createdAt?: string,
      updatedAt?: string
    }
  ],
  stopPagination?: boolean,
  message?: string
}
```

Only the `id` and `name` fields are required. The `type` field is required for files.

**Description:**

- `id` - unique identifier of the file or folder.
- `name` - name of the file or folder.
- `type`- type of the file (`json`, `xml`, etc.). If you skip this, the item will be considered as a folder.
- `parentId` - identifier of the parent folder.
- `nodeType` - type of the node (`0` - folder, `1` - file, `2` - branch).
- `labels` - custom labels for the file entry.
  - `text` - label text.
  - `type` - label type (`primary | secondary | success | warning | info | danger | dark | light`).
  - `color` - label color.
- `customContent` - custom HTML content for the file entry.
- `disabled` - disables item selection and visually indicates it's unavailable
- `tooltip` - hover text providing context about the item's purpose or status
- `stopPagination` - stop pagination if there is no more data to return in case you use pagination for the integration files.
- `message` - custom message.
- `createdAt` - Timestamp when the file was created (for `isNew` filtering)
- `updatedAt` - Timestamp when the file was last updated (for `isUpdated` filtering)

#### Load More Pagination

For integrations that need to fetch large amounts of data from external services, you can implement dynamic pagination using the "Load More" pattern. This allows users to load additional items on demand without overwhelming the initial view.

##### How It Works

1. When your integration API returns too many items, you can return a subset along with a special "load-more" element
2. The "load-more" element contains a `pagination` object with custom data needed to fetch the next page
3. When the user clicks "Load More", Crowdin calls `getIntegrationFiles` again with the `paginationData` parameter
4. Your integration uses this data to fetch and return the next batch of items

##### Implementation Example

```js title="index.js"
getIntegrationFiles: async ({
  credentials,
  client,
  projectId,
  settings,
  parentId,
  search,
  page,
  paginationData
}) => {
  const PAGE_SIZE = 50;
  
  // Get offset from pagination data or start from 0
  const offset = paginationData?.offset || 0;
  
  // Fetch data from your external service
  const response = await fetch(`https://api.example.com/files?parent=${parentId}&offset=${offset}&limit=${PAGE_SIZE}`);
  const { items, hasMore } = await response.json();
  
  // Transform items to Crowdin format
  const files = items.map(item => ({
    id: item.id,
    name: item.name,
    type: item.type,
    parentId: item.parentId,
    nodeType: item.isFolder ? '0' : '1'
  }));
  
  // Add "Load More" element if there are more items
  if (hasMore) {
    files.push({
      id: `load-more`,
      name: 'Load More',
      nodeType: 'load-more',
      node_type: 'load-more',
      parent_id: parentId,
      pagination: {
        offset: offset + PAGE_SIZE
      }
    });
  }
  
  return { data: files };
}
```

##### Load More Element Structure

The "load-more" element must have the following structure:

```js
{
  id: string,
  name: string,
  nodeType: 'load-more',
  node_type: 'load-more',
  parent_id: string,
  pagination: {
    // Your custom fields - these will be passed back in paginationData
  }
}
```

### `updateCrowdin` Function

This function is used to update the Crowdin project with the data from the integration. Here you need to get data from the integration and upload it to Crowdin.

##### Parameters

- `projectId` - Crowdin project ID.
- `client` - Crowdin API client.
- `credentials` - Crowdin API credentials.
- `request` - Request object.
- `rootFolder` - Root folder identifier.
- `settings` - Application settings.
- `uploadTranslations` - Boolean value indicating whether the user has enabled the option to upload existing translations from the integration to Crowdin.
- `job` - [Job object](/app-project-module/reference/job/).
- `excludedTargetLanguages` - Array of language IDs to exclude from uploading translations to Crowdin.

##### Return Value

By default, the function should return nothing. If you want to display a custom message after executing the update process, you can return an object with the `message` field:

```js
return {
  message: 'Some message'
};
```

### `updateIntegration` Function

This function is used to update the integration with the data from Crowdin. Here you need to get translations from Crowdin and upload them to the integration.

##### Parameters

- `projectId` - Crowdin project ID.
- `client` - Crowdin API client.
- `credentials` - Crowdin API credentials.
- `request` - Request object.
- `rootFolder` - Root folder identifier.
- `settings` - Application settings.
- `job` - [Job object](/app-project-module/reference/job/).

#### Upload Only New Translations

You can configure the integration to upload only new translations. To do this, you need to use the `job.fetchTranslation` method to get the link to download the translation file. After uploading the translation to the integration, you need to call the `job.translationUploaded` method to notify Crowdin that the translation has been uploaded.

By default, `job.fetchTranslation` uses eTag to skip translations that haven't changed since the last sync. If you need to provide users with the ability to force push translations regardless of eTag status, you can enable the force push button by setting `configuration.projectIntegration.forcePushTranslations: true` in your configuration. This is useful when you want to ensure translations are pushed even if they haven't changed according to eTag.

```js {23,37} wrap title="index.js"
updateIntegration: async ({
  projectId,
  client,
  credentials,
  request,
  rootFolder,
  settings,
  job,
}) => {
  const directories = await client.sourceFilesApi
    .withFetchAll()
    .listProjectDirectories(projectId);
  const { files } = await crowdinAppFunctions.getFolder({
    directories: directories.data.map((d) => d.data),
    client,
    projectId,
    directoryName: 'Folder from integration',
    parentDirectory: rootFolder
  });

  const file = files.find((f) => f.name === 'integration.json');
  if (file) {
    // Fetches only the new translations
    const link = job.fetchTranslation({
      fileId: file.id,
      languageId: 'uk',
      params: {
        skipUntranslatedStrings: true
      }
    });

    if (!link) {
      return;
    }

    const response = await axios.get(link.data.url);

    // upload translation logic

    // If using job.fetchTranslation, call this method after successfully uploading the translation to the service
    await job.translationUploaded({
      fileId: file.id,
      translationParams: [{
        languageId: 'uk',
        etag: link.data.etag
      }]
    });
  }
}
```

#### Unsynced Files

Files with synchronization problems can be marked as unsynced. This helps users easily identify and manage problematic files.
```js {35} wrap

configuration.projectIntegration.updateIntegration: async ({
  projectId,
  client,
  credentials,
  request,
  rootFolder,
  settings,
  job,
}) => {
  const unsyncedFiles = [];

  const file = files.find((f) => f.name === 'integration.json');
  try {
    if (file) {
      // upload translation logic
    }
  } catch(e) {
    if (e.response && e.response.status === 404) {
      unsyncedFiles.push({ fileId: file.id, error: e });
    }
  }

  // Marks the specified files as unsynced, indicating they failed to synchronize properly
  await job.markFilesAsUnsynced({ files: unsyncedFiles });
}
```

### `getFileProgress` Function

It is possible to customize the translation progress for each file by using the `getFileProgress` function.

##### Parameters

- `projectId` - Crowdin project ID.
- `client` - Crowdin API client.
- `fileId` - File ID.

##### Return Value

The API endpoint returns the translation progress in the following format:

```json
{
  "data": {
    "[fileId]": [
      {
        "languageId": "string",
        "eTag": "string",
        "language": {
          "id": "string",
          "name": "string",
          "editorCode": "string",
          "twoLettersCode": "string",
          "threeLettersCode": "string",
          "locale": "string",
          "androidCode": "string",
          "osxCode": "string",
          "osxLocale": "string",
          "pluralCategoryNames": ["string"],
          "pluralRules": "string",
          "pluralExamples": ["string"],
          "textDirection": "ltr | rtl",
          "dialectOf": "string | null"
        },
        "words": {
          "total": 0,
          "translated": 0,
          "preTranslateAppliedTo": 0,
          "approved": 0
        },
        "phrases": {
          "total": 0,
          "translated": 0,
          "preTranslateAppliedTo": 0,
          "approved": 0
        },
        "translationProgress": "0",
        "approvalProgress": "0",
        "qaChecksStatus": {
          "total": 0,
          "inProgress": 0,
          "passed": 0,
          "failed": 0
        }
      }
    ]
  }
}
```

**Example Response:**

```json
{
  "data": {
    "78866": [
      {
        "languageId": "uk",
        "eTag": "35ddc6c8f634f062e0de95c1c159f59d",
        "language": {
          "id": "uk",
          "name": "Ukrainian",
          "editorCode": "uk",
          "twoLettersCode": "uk",
          "threeLettersCode": "ukr",
          "locale": "uk-UA",
          "androidCode": "uk-rUA",
          "osxCode": "uk.lproj",
          "osxLocale": "uk",
          "pluralCategoryNames": ["one", "few", "many", "other"],
          "pluralRules": "((n%10==1 && n%100!=11) ? 0 : ...)",
          "pluralExamples": ["1, 21, 31...", "2-4, 22-24..."],
          "textDirection": "ltr",
          "dialectOf": null
        },
        "words": {
          "total": 9,
          "translated": 0,
          "preTranslateAppliedTo": 0,
          "approved": 0
        },
        "phrases": {
          "total": 3,
          "translated": 0,
          "preTranslateAppliedTo": 0,
          "approved": 0
        },
        "translationProgress": "0",
        "approvalProgress": "0",
        "qaChecksStatus": {
          "total": 0,
          "inProgress": 0,
          "passed": 0,
          "failed": 0
        }
      }
    ]
  }
}
```

If implementing a custom `getFileProgress` function, return data in this format (the API endpoint will wrap it automatically):

```yml
{
  [fileId]: [
    {
      languageId: string,
      eTag: string,
      language: LanguageObject,
      words: { total, translated, preTranslateAppliedTo, approved },
      phrases: { total, translated, preTranslateAppliedTo, approved },
      translationProgress: string,
      approvalProgress: string,
      qaChecksStatus: { total, inProgress, passed, failed }
    }
  ]
}
```

### `matchCrowdinFilesToIntegrationFiles` Function

This function enables bidirectional file matching between Crowdin and your integration. When implemented, it adds new sync options to both the integration and Crowdin side menus, allowing users to sync files in either direction.

##### Parameters

- `projectId` - Crowdin project ID.
- `client` - Crowdin API client.
- `credentials` - Integration credentials.
- `crowdinFiles` - Object mapping Crowdin file IDs to language arrays (when syncing from Crowdin side).
- `integrationFiles` - Array of integration files (when syncing from integration side).
- `settings` - Application settings.
- `rootFolder` - Root folder in Crowdin (if configured).
- `syncedData` - Previously synced data for reference.

##### Return Value

Should return matched files in the appropriate format, or an object containing both data and any errors:

```js
// Simple return - just matched files
return matchedFiles;

// With errors
return {
  data: matchedFiles,
  errors: [
    new Error('File "example.json" could not be matched')
  ]
};
```

When errors are returned the sync will complete for successfully matched files, and warnings will be displayed to the user.

##### Example Implementation

```js title="index.js"
matchCrowdinFilesToIntegrationFiles: async ({
  projectId,
  client,
  crowdinFiles,
  integrationFiles,
  rootFolder,
  syncedData,
}) => {
  const errors = [];

  if (integrationFiles) {
    const files = (await client.sourceFilesApi.withFetchAll().listProjectFiles(projectId, {
      directoryId: rootFolder?.id,
      recursion: true,
    }))?.data.map(f => f.data) || [];

    const project = await client.projectsGroupsApi.getProject(projectId);
    const targetLanguageIds = project.data.targetLanguageIds;
    const fileToLanguagesMap = {};

    for (const integrationFile of integrationFiles) {
      const crowdinFile = files.find(f => f.name === `${integrationFile.id}.json`);
      if (crowdinFile) {
        fileToLanguagesMap[crowdinFile.id] = targetLanguageIds;
      } else {
        errors.push(new Error(`No Crowdin file found for "${integrationFile.name}"`));
      }
    }

    return { data: fileToLanguagesMap, errors };
  }

  if (crowdinFiles) {
    const result = [];

    for (const fileId of Object.keys(crowdinFiles)) {
      const syncedFile = syncedData.find(s => s.crowdinFileId === parseInt(fileId));
      if (syncedFile) {
        const { crowdinFileId, ...integrationFileData } = syncedFile;
        result.push(integrationFileData);
      } else {
        errors.push(new Error(`Crowdin file ${fileId} not found in synced data`));
      }
    }

    return { data: result, errors };
  }
}
```

### `onLogout` Function

This function is called when the user logs out from the integration. You can use it to clean up any resources or perform other necessary actions.

##### Parameters

- `projectId` - Crowdin project ID.
- `client` - Crowdin API client.
- `credentials` - Crowdin API credentials.
- `settings` - Application settings.

### User Errors Retention Period

Configure the retention period for user errors, with automatic daily deletion:

```js title="index.js"
configuration.projectIntegration.userErrorLifetimeDays = 7
```

The default value is 30 days.

### Info Window

You can define a section with some information notes or a help section for your application:

```js title="index.js"
configuration.projectIntegration.infoModal = {
  title: 'Info',
  content: `
    <h1>This is your app help section</h1>
    </br>
    <h2>This is just an example</h2>
  `
}
```

### Notice

You can also set notifications at the top of the screen with important information for your application:

```js wrap title="index.js"
configuration.projectIntegration.notice = {
    title: 'Upgrade Alert',
    content: `We recommend using a <a target="_blank" href="https://store.crowdin.com/">newer version</a> as this application is no longer supported.`,
    type: 'info',
    icon: true,
    close: false
}
```

Possible notification types: `info | warning | danger | success | error | dataLostWarning`.

### Progressive Loading

For projects with many files, you can enable progressive loading to improve the user experience. When enabled, the directory structure loads first, followed by files loading separately. This provides users with immediate visibility of the folder structure while files continue to load in the background.

To enable progressive loading:

```js title="index.js"
configuration.projectIntegration.progressiveCrowdinFilesLoading = true;
```

By default, this feature is disabled (`false`) to maintain backward compatibility. When disabled, all files and directories are loaded in a single request as before.

**Custom implementation:**
If you have a custom `getCrowdinFiles` function, it will receive an optional `mode` parameter:
- `mode: 'directories'` - Load only directories and branches
- `mode: 'files'` - Load only files
- `mode: undefined` - Load everything (default behavior)

Example of custom implementation:

```js title="index.js"
configuration.projectIntegration.getCrowdinFiles = async ({
  projectId,
  client,
  rootFolder,
  settings,
  mode
}) => {
  if (mode === 'directories') {
    // Return only directories and branches
    return await loadDirectories(projectId, client);
  } else if (mode === 'files') {
    // Return only files
    return await loadFiles(projectId, client);
  } else {
    // Load everything (default)
    return await loadAll(projectId, client);
  }
};
```

### Snapshot Fetch Concurrency

When `integrationOneLevelFetching` is enabled, the snapshot builder recursively traverses the integration file tree. By default, folders at each level are processed one at a time to avoid overwhelming the external API with concurrent requests, which can cause rate-limiting or gateway errors in deep or wide trees.

To increase parallelism for faster snapshot building:

```js title="index.js"
configuration.projectIntegration.snapshotFetchConcurrency = 5;
```

The default value is `1` (sequential processing). Use higher values with caution — the effective concurrency grows with tree depth, so setting this to `5` on a tree with 3 levels of nesting can result in up to `5³ = 125` simultaneous requests at the deepest level.

### Filters

To disable language filtering, add the following configuration:

```js title="index.js"
configuration.projectIntegration.filtering = {
  crowdinLanguages: false
}
```

You can configure file status filtering to show files based on their sync status:

```js title="index.js"
configuration.projectIntegration.filtering = {
  integrationFileStatus: {
    isNew: true,        // Show files that are new since last sync (requires createdAt timestamp)
    isUpdated: true,    // Show files that were updated since last sync (requires updatedAt timestamp)
    notSynced: true,    // Show files that were never synced (defaults to true when integrationOneLevelFetching is false)
    synced: true        // Show files that were previously synced to Crowdin
  }
}
```

For `isNew` and `isUpdated` filtering to work, your `getIntegrationFiles` function must return files with the following additional fields:
- `createdAt`: Timestamp when the file was created (for `isNew` filtering)
- `updatedAt`: Timestamp when the file was last updated (for `isUpdated` filtering)

Example of a file object with required fields:
```js
{
  id: '12',
  name: 'File from integration',
  type: 'json',
  parentId: '10',
  createdAt: '2024-03-20T10:00:00Z',  // Required for isNew filtering
  updatedAt: '2024-03-20T15:30:00Z'   // Required for isUpdated filtering
}
```

If you need to filter files, to skip some of them, you can use the following configuration:

```js title="index.js"
configuration.projectIntegration.skipIntegrationNodes = {
  fileNamePattern: '\\([\\w]{2}[-|_][\\w]{2,3}\\)$',
  folderNamePattern: '\\([\\w]{2}[-|_][\\w]{2,3}\\)$'
}
```

Description:

- `fileNamePattern` - regular expression to skip files. Optional.
- `folderNamePattern` - regular expression to skip folders. Optional.

You can control whether the skipIntegrationNodes is applied using the `skipIntegrationNodesToggle` setting:

```js title="index.js"
configuration.projectIntegration.skipIntegrationNodesToggle = {
  title: 'Skip Integration Nodes',
  description: 'Skip certain files and folders that should not be synchronized (e.g., language-specific versions, temporary files)',
  value: true
}
```

### Smart Source File Synchronization

The SDK automatically tracks when each file was last synchronized and only pushes files that have been modified since their last sync. This improves performance and reduces unnecessary API calls.

**How it works:**
- Before syncing, it compares the file's `updatedAt` timestamp with the last sync timestamp
- Only files that have been modified are pushed to Crowdin
- Users can override this behavior using the "Force Sync" button

**Enabling Force Push:**

To give users the ability to force synchronize all files regardless of their modification status, enable the `forcePushSources` option:

```js title="index.js"
configuration.projectIntegration.forcePushSources = true;
```

When enabled, a "Force Sync" button appears in the integration menu, allowing users to:
- Bypass the automatic filtering of unchanged files
- Push all selected files to Crowdin, even if they haven't been modified

**Requirements:**

For this feature to work properly, your `getIntegrationFiles` function must return files with the `updatedAt` field:

```js
{
  id: '12',
  name: 'File from integration',
  type: 'json',
  parentId: '10',
  createdAt: '2024-03-20T10:00:00Z',
  updatedAt: '2024-03-20T15:30:00Z'  // Required for smart sync
}
```

## Token Management for Long Operations

When processing many files, languages, or performing other long-running operations in your integration functions (`getIntegrationFiles`, `updateCrowdin`, `updateIntegration`, cron jobs, etc.), the operation might take longer than the token lifetime. If your integration has `refresh: true` enabled in the [authorization configuration](/app-project-module/project-integration/authorization/), a `tokenProvider` object is automatically added to the credentials parameter.

The `tokenProvider` provides two methods:

- `getToken()` - Returns a valid token, automatically refreshing if expired (with 2-minute buffer)
- `refreshToken()` - Forces a token refresh and returns the new token

### Usage Example

Use `credentials.tokenProvider.getToken()` to get a fresh token before making API calls to your integration service:

```js title="index.js"
// Example in updateIntegration
updateIntegration: async ({ credentials, request, job }) => {
  for (const [fileId, targetLanguages] of Object.entries(request)) {
    for (const languageId of targetLanguages) {
      // Get fresh token before each API call during long operations
      const token = await credentials.tokenProvider.getToken()

      // Make API call to external service
      const response = await fetch(`https://external-api.com/files/${fileId}`, {
        headers: {
          'Authorization': `Bearer ${token}`,
          'Content-Type': 'application/json'
        }
      });
    }
  }
},

// Example in getIntegrationFiles
getIntegrationFiles: async ({ credentials, client, projectId, settings, parentId }) => {
  const token = await credentials.tokenProvider.getToken();

  const response = await fetch(`https://external-api.com/files`, {
    headers: { 'Authorization': `Bearer ${token}` }
  });

  return { data: response.data };
}

```

### Automatic Token Management

The `tokenProvider` automatically handles:
- Token expiration detection (with 2-minute buffer)
- Refresh token requests using your configured refresh logic
- Database credential updates

**Note**: The `tokenProvider` is only available when `refresh: true` is set in either `loginForm` or `oauthLogin` configuration. For integrations without refresh capability, use `credentials.accessToken` directly.

## Testing

Crowdin Apps SDK provides a set of testing utilities to help you test your integration app.

See [Testing](/app-project-module/testing/) for more information.

## API

The Project Integration module implements API methods. For a full list of available endpoints, visit the [Default Endpoints for Project Integration](/app-project-module/tools/api/#default-endpoints-for-project-integration).