UNPKG

@crowdin/app-project-module

Version:

Module that generates for you all common endpoints for serving standalone Crowdin App

594 lines (591 loc) 28.1 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getRootFolder = getRootFolder; exports.getOauthRoute = getOauthRoute; exports.applyIntegrationModuleDefaults = applyIntegrationModuleDefaults; exports.constructOauthUrl = constructOauthUrl; exports.getOAuthPollingId = getOAuthPollingId; exports.getOAuthLoginFormId = getOAuthLoginFormId; exports.groupFieldsByCategory = groupFieldsByCategory; exports.buildMenuItems = buildMenuItems; const crowdin_1 = require("../../../util/app-functions/crowdin"); const types_1 = require("../types"); function getRootFolder(config, integration, client, projectId) { return __awaiter(this, void 0, void 0, function* () { if (!integration.withRootFolder) { return; } const folder = integration.appFolderName || config.name; const directories = (yield client.sourceFilesApi.withFetchAll().listProjectDirectories(projectId)).data.map((d) => d.data); const { folder: rootFolder } = yield (0, crowdin_1.getOrCreateFolder)({ directories, client, projectId, directoryName: folder, }); return rootFolder; }); } function getOauthRoute(integration) { var _a; return ((_a = integration.oauthLogin) === null || _a === void 0 ? void 0 : _a.redirectUriRoute) || '/oauth/code'; } function applyIntegrationModuleDefaults(config, integration) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o; if (!integration.getCrowdinFiles) { integration.getCrowdinFiles = (_a) => __awaiter(this, [_a], void 0, function* ({ projectId, client, rootFolder, mode }) { let options = {}; if (rootFolder) { options = { directoryId: rootFolder.id, recursion: 'true', }; } const needDirectories = !mode || mode === 'directories' || mode === 'files'; const needFiles = !mode || mode === 'files'; const [branchesResponse, directoriesResponse, filesResponse] = yield Promise.all([ client.sourceFilesApi.withFetchAll().listProjectBranches(projectId), needDirectories ? client.sourceFilesApi.withFetchAll().listProjectDirectories(projectId, options) : { data: [] }, needFiles ? client.sourceFilesApi.withFetchAll().listProjectFiles(projectId, options) : { data: [] }, ]); const allBranches = branchesResponse.data.map((d) => d.data); const allDirectories = directoriesResponse.data.map((d) => d.data); const allFiles = filesResponse.data.map((d) => d.data); const branchesMap = new Map(allBranches.map((b) => [b.id, b])); const addedBranchIds = new Set(); const res = []; const addBranchIfNeeded = (branchId) => { const branch = branchesMap.get(branchId); if (branch && !addedBranchIds.has(branch.id)) { addedBranchIds.add(branch.id); res.push({ id: branch.id.toString(), name: branch.name, nodeType: '2', }); } return branch === null || branch === void 0 ? void 0 : branch.id; }; if (!mode || mode === 'directories') { allDirectories.forEach((e) => { let parentId = rootFolder && e.directoryId === rootFolder.id ? undefined : e.directoryId; if (!parentId && e.branchId) { parentId = addBranchIfNeeded(e.branchId); } res.push({ id: e.id.toString(), parentId: parentId ? parentId.toString() : undefined, name: e.name, }); }); } if (!mode || mode === 'files') { const directoryIds = mode === 'files' ? allDirectories.map((d) => d.id) : res.filter((item) => !item.type && item.nodeType !== '2').map((d) => parseInt(d.id)); const filteredFiles = allFiles.filter((f) => (rootFolder && f.directoryId === rootFolder.id) || directoryIds.includes(f.directoryId) || (!rootFolder && !f.directoryId)); filteredFiles.forEach((e) => { let parentId = rootFolder && e.directoryId === rootFolder.id ? undefined : e.directoryId; if (!parentId && e.branchId) { parentId = addBranchIfNeeded(e.branchId); } res.push({ id: e.id.toString(), parentId: parentId ? parentId.toString() : undefined, name: e.title || e.name, type: e.type, excludedTargetLanguages: e.excludedTargetLanguages, }); }); } return res; }); } if (!integration.getFileProgress) { integration.getFileProgress = (_a) => __awaiter(this, [_a], void 0, function* ({ projectId, client, fileId }) { const progress = yield client.translationStatusApi.withFetchAll().getFileProgress(projectId, fileId); return { [fileId]: progress.data.map((e) => e.data) }; }); } if (!integration.updateFilesTargetLanguages) { integration.updateFilesTargetLanguages = (_a) => __awaiter(this, [_a], void 0, function* ({ projectId, client, fileIds, excludedTargetLanguages }) { for (const fileId of fileIds) { yield client.sourceFilesApi.editFile(projectId, fileId, [ { op: 'replace', path: '/excludedTargetLanguages', value: excludedTargetLanguages, }, ]); } }); } if (!integration.oauthLogin && !integration.loginForm) { integration.loginForm = { fields: [ { helpText: 'You need to create standard api key', key: 'apikey', label: `${config.name} API Key`, }, ], }; } if ((integration.filterByPathIntegrationFiles === undefined || integration.filterByPathIntegrationFiles) && !integration.integrationOneLevelFetching) { integration.filterByPathIntegrationFiles = true; } const getUserSettings = integration.getConfiguration; integration.getConfiguration = (_a) => __awaiter(this, [_a], void 0, function* ({ projectId, client: crowdinClient, credentials, settings, clientId, }) { var _b, _c, _d; let fields = []; const project = (yield crowdinClient.projectsGroupsApi.getProject(projectId)); if (getUserSettings) { fields = yield getUserSettings({ projectId, client: crowdinClient, credentials, settings, clientId, }); } const defaultSettings = []; if (project.data.inContext) { defaultSettings.push({ key: 'inContext', label: 'Sync In-Context Pseudo Language', type: 'checkbox', defaultValue: 'false', category: types_1.DefaultCategory.ADVANCED, position: 3, helpTextHtml: ` <p><strong>What is In-Context?</strong></p> <p>Crowdin In-Context allows translators to see and edit translations directly on your live website or application.</p> <p><strong>Enable this setting if:</strong></p> <ul style="margin: 8px 0; padding-left: 20px;"> <li>You have integrated the Crowdin In-Context script in your website/app</li> <li>You want to sync the pseudo-language for in-context translation testing</li> </ul> <p style="margin-top: 8px;"> <a href="https://support.crowdin.com/in-context-localization/" target="_blank" rel="noopener noreferrer"> Learn more about In-Context → </a> </p> `, }); } if (integration.withCronSync || integration.webhooks) { defaultSettings.push({ key: 'auto-sync-info', type: 'notice', label: 'How Auto Sync works', helpTextHtml: ` <p><strong>Auto Sync is disabled by default.</strong> Here's how to enable it:</p> <ol style="margin: 8px 0 0 0; padding-left: 20px;"> <li><strong>Choose a schedule</strong> below (how often to sync)</li> <li><strong>Select files:</strong> In the file browser, right-click on files or folders</li> <li>Click <strong>"Enable Auto Sync"</strong> from the context menu</li> </ol> <p style="margin: 8px 0 0 0;"> 💡 Only files/folders with Auto Sync enabled will be synchronized automatically. </p> `, category: types_1.DefaultCategory.SYNC, position: -1, }); const userSchedule = fields.find((field) => 'key' in field && field.key === 'schedule'); if (userSchedule) { userSchedule.position = (_b = userSchedule.position) !== null && _b !== void 0 ? _b : 0; userSchedule.category = types_1.DefaultCategory.SYNC; } else { defaultSettings.push({ key: 'schedule', label: 'Sync schedule', helpText: `Defines how often content is synced between ${config.name} and Crowdin. Make sure Auto Sync is enabled for selected directories and files in the dual pane view.`, type: 'select', defaultValue: '0', category: types_1.DefaultCategory.SYNC, position: 0, options: [ { value: '0', label: 'Disabled', }, { value: '1', label: '1 hour', }, { value: '3', label: '3 hours', }, { value: '6', label: '6 hours', }, { value: '12', label: '12 hours', }, { value: '24', label: '24 hours', }, ], }); } if ((_c = integration.syncNewElements) === null || _c === void 0 ? void 0 : _c.crowdin) { defaultSettings.push({ key: 'new-crowdin-files', label: 'Automatically sync new translations from Crowdin', type: 'checkbox', helpText: 'Automatically include newly added Crowdin files and folders in Auto Sync.', dependencySettings: JSON.stringify([{ '#schedule-settings': { type: '!equal', value: ['0'] } }]), category: types_1.DefaultCategory.SYNC, position: 1, }); } if ((_d = integration.syncNewElements) === null || _d === void 0 ? void 0 : _d.integration) { defaultSettings.push({ key: 'new-integration-files', label: `Automatically sync new content from ${config.name}`, type: 'checkbox', helpText: `Automatically include new ${config.name} files and folders in Auto Sync as soon as they appear.`, dependencySettings: JSON.stringify([{ '#schedule-settings': { type: '!equal', value: ['0'] } }]), category: types_1.DefaultCategory.SYNC, position: 2, }); } if (integration.uploadTranslations) { defaultSettings.push({ labelHtml: `<b>Translation sync settings (${config.name} → Crowdin)</b>`, category: types_1.DefaultCategory.SYNC, position: 3, }, { key: 'importEqSuggestions', label: 'Add translations that are the same as the source text', type: 'checkbox', helpText: 'Upload translations that match the source text. Useful when the source and target are intentionally identical, for example brand names, numbers, or code snippets.', category: types_1.DefaultCategory.SYNC, position: 4, }, { key: 'autoApproveImported', label: 'Mark added translations as Approved in Crowdin', type: 'checkbox', helpText: 'Mark uploaded translations as Approved in Crowdin, bypassing proofreading. Leave off if your workflow requires a reviewer to approve translations manually.', category: types_1.DefaultCategory.SYNC, position: 5, }, { key: 'translateHidden', label: 'Add translations for hidden source strings in Crowdin', type: 'checkbox', helpText: 'Also upload translations for source strings marked as hidden in Crowdin. Hidden strings are excluded from the translator workflow but can still receive translations from your integration.', category: types_1.DefaultCategory.SYNC, position: 6, }); } defaultSettings.push({ key: 'condition', label: 'Files export settings', type: 'select', defaultValue: '0', helpText: 'Decides which Crowdin files are exported on each sync. "Export all" includes every file; "Export translated only" skips files with no translations; "Export approved only" requires translations to be approved by a proofreader.', dependencySettings: JSON.stringify([{ '#schedule-settings': { type: '!equal', value: ['0'] } }]), category: types_1.DefaultCategory.SYNC, position: 7, options: [ { value: '0', label: 'Export all', }, { value: '1', label: 'Export translated only', }, { value: '2', label: 'Export approved only', }, ], }); } if (integration.filterByPathIntegrationFiles) { defaultSettings.push({ label: 'File Filters', category: types_1.DefaultCategory.ADVANCED, position: 0, }, { key: 'includeByFilePath', label: 'Show only files matching pattern', type: 'textarea', helpTextHtml: ` <p>Enter file path patterns to include for synchronization. Use wildcard selectors:</p> <ul style="margin: 8px 0; padding-left: 20px;"> <li><code>*</code> - matches any characters except /</li> <li><code>**</code> - matches any characters including /</li> </ul> <p><strong>Examples:</strong></p> <ul style="margin: 8px 0; padding-left: 20px;"> <li><code>/pages/**</code> - all files in pages folder</li> <li><code>/**/*.json</code> - all JSON files</li> <li><code>/content/**/*.md</code> - all Markdown files in content</li> </ul> `, category: types_1.DefaultCategory.ADVANCED, position: 1, }, { key: 'excludeByFilePath', label: 'Hide files matching pattern', type: 'textarea', helpTextHtml: ` <p>Enter path patterns for files or folders to exclude. Use wildcard selectors:</p> <ul style="margin: 8px 0; padding-left: 20px;"> <li><code>*</code> - matches any characters except /</li> <li><code>**</code> - matches any characters including /</li> </ul> <p><strong>Examples:</strong></p> <ul style="margin: 8px 0; padding-left: 20px;"> <li><code>/drafts/**</code> - ignore drafts folder</li> <li><code>/**/.git/**</code> - ignore all .git folders</li> <li><code>/**/test/**</code> - ignore all test folders</li> </ul> `, category: types_1.DefaultCategory.ADVANCED, position: 2, }); } if (integration.skipIntegrationNodes && integration.skipIntegrationNodesToggle) { defaultSettings.push({ key: 'skipIntegrationNodesToggle', label: integration.skipIntegrationNodesToggle.title, type: 'checkbox', helpText: integration.skipIntegrationNodesToggle.description, defaultValue: integration.skipIntegrationNodesToggle.value, category: types_1.DefaultCategory.GENERAL, }); } return [...defaultSettings, ...fields]; }); if (!integration.checkConnection) { integration.checkConnection = (credentials) => __awaiter(this, void 0, void 0, function* () { yield integration.getIntegrationFiles({ credentials }); }); } if (integration.webhooks && !((_a = integration.webhooks) === null || _a === void 0 ? void 0 : _a.urlParam)) { integration.webhooks.urlParam = 'crowdinData'; } if (!((_b = integration.filtering) === null || _b === void 0 ? void 0 : _b.hasOwnProperty('crowdinLanguages'))) { integration.filtering = Object.assign(Object.assign({}, (integration.filtering || {})), { crowdinLanguages: true }); } integration.filtering.integrationFileStatus = Object.assign(Object.assign({}, (integration.integrationOneLevelFetching ? {} : { notSynced: true })), integration.filtering.integrationFileStatus); if (!((_c = integration.filtering) === null || _c === void 0 ? void 0 : _c.hasOwnProperty('integrationFilterConfig'))) { const filterItems = [ { value: 'all', label: 'All', }, ]; if ((_e = (_d = integration.filtering) === null || _d === void 0 ? void 0 : _d.integrationFileStatus) === null || _e === void 0 ? void 0 : _e.isNew) { filterItems.push({ value: 'isNew', label: 'New', }); } if ((_g = (_f = integration.filtering) === null || _f === void 0 ? void 0 : _f.integrationFileStatus) === null || _g === void 0 ? void 0 : _g.isUpdated) { filterItems.push({ value: 'isUpdated', label: 'Modified', }); } if ((_j = (_h = integration.filtering) === null || _h === void 0 ? void 0 : _h.integrationFileStatus) === null || _j === void 0 ? void 0 : _j.failed) { filterItems.push({ value: 'failed', label: 'Sync Error', }); } if ((_l = (_k = integration.filtering) === null || _k === void 0 ? void 0 : _k.integrationFileStatus) === null || _l === void 0 ? void 0 : _l.notSynced) { filterItems.push({ value: 'notSynced', label: 'Never Synced', }); } if ((_o = (_m = integration.filtering) === null || _m === void 0 ? void 0 : _m.integrationFileStatus) === null || _o === void 0 ? void 0 : _o.synced) { filterItems.push({ value: 'synced', label: 'Previously Synced', }); } integration.filtering = Object.assign(Object.assign({}, (integration.filtering || {})), { integrationFilterConfig: filterItems.length > 1 ? [ { key: 'file', type: 'list_single', label: 'File', items: filterItems, defaultValue: 'all', defaultLabel: 'All', }, ] : [] }); } if (!integration.userErrorLifetimeDays) { integration.userErrorLifetimeDays = 30; } integration.jobStoreType = integration.jobStoreType || 'db'; } function constructOauthUrl({ config, integration, clientId, loginForm, }) { var _a, _b, _c, _d, _e; const oauth = integration.oauthLogin; if (!oauth) { return; } if (oauth.getAuthorizationUrl) { if (!loginForm) { return; } let url = oauth.getAuthorizationUrl({ redirectUrl: `${config.baseUrl}${getOauthRoute(integration)}`, loginForm, }); url += `&${((_a = oauth.fieldsMapping) === null || _a === void 0 ? void 0 : _a.state) || 'state'}=${Buffer.from(clientId).toString('base64')}`; return url; } if (!oauth.authorizationUrl) { return; } let url = oauth.authorizationUrl || ''; url += `?${((_b = oauth.fieldsMapping) === null || _b === void 0 ? void 0 : _b.clientId) || 'client_id'}=${oauth.clientId}`; url += `&${((_c = oauth.fieldsMapping) === null || _c === void 0 ? void 0 : _c.redirectUri) || 'redirect_uri'}=${config.baseUrl}${getOauthRoute(integration)}`; url += `&${((_d = oauth.fieldsMapping) === null || _d === void 0 ? void 0 : _d.state) || 'state'}=${Buffer.from(clientId).toString('base64')}`; if (oauth.scope) { url += `&${((_e = oauth.fieldsMapping) === null || _e === void 0 ? void 0 : _e.scope) || 'scope'}=${oauth.scope}`; } if (oauth.extraAutorizationUrlParameters) { Object.entries(oauth.extraAutorizationUrlParameters).forEach(([key, value]) => (url += `&${key}=${value}`)); } return url; } function getOAuthPollingId(clientId) { return `oauth_${clientId}`; } function getOAuthLoginFormId(clientId) { return `oauth_form_${clientId}`; } function groupFieldsByCategory(fields) { const groupedFields = fields.reduce((acc, field) => { const category = (field === null || field === void 0 ? void 0 : field.category) || types_1.DefaultCategory.GENERAL; if (!acc[category]) { acc[category] = []; } acc[category].push(field); return acc; }, {}); // Sort fields by position within each category Object.keys(groupedFields).forEach((category) => { groupedFields[category].sort((a, b) => { // If neither has position, maintain original order if (!('position' in a) && !('position' in b)) { return 0; } // If only one has position, the one without position goes to the end if (!('position' in a)) { return 1; } if (!('position' in b)) { return -1; } // If both have position, sort by position value (lower numbers first) return (a.position || 0) - (b.position || 0); }); }); // Define category order: General Settings → Sync settings → Advanced const categoryOrder = { [types_1.DefaultCategory.GENERAL]: 1, [types_1.DefaultCategory.SYNC]: 2, [types_1.DefaultCategory.ADVANCED]: 3, }; return Object.entries(groupedFields) .sort(([categoryA], [categoryB]) => { const orderA = categoryOrder[categoryA] || 999; const orderB = categoryOrder[categoryB] || 999; return orderA - orderB; }) .map(([category, fields]) => ({ name: category, fields, })); } function buildMenuItems(integration, appName) { const integrationMenuItems = []; if (integration.matchCrowdinFilesToIntegrationFiles) { integrationMenuItems.push({ label: `Translations: Sync to ${appName}`, title: `Sync translations to ${appName}`, action: 'syncTranslationsToIntegration', }); } if (integration.uploadTranslations) { integrationMenuItems.push({ label: 'Translations: Sync to Crowdin', title: 'Sync translations to Crowdin', action: 'uploadTranslations', }); } if (integration.forcePushSources) { integrationMenuItems.push({ label: 'Source Files: Sync to Crowdin (Forced)', title: 'Force sync files to Crowdin', action: 'forcePushSources', }); } if (integration.excludedTargetLanguages) { integrationMenuItems.push({ label: 'Change Files Target Languages', title: 'Upload file for translation into selected languages', action: 'excludedTargetLanguages', }); } const crowdinMenuItems = []; if (integration.matchCrowdinFilesToIntegrationFiles) { crowdinMenuItems.push({ label: 'Source Files: Sync to Crowdin', action: 'syncSourcesToCrowdin', }); } if (integration.matchCrowdinFilesToIntegrationFiles) { crowdinMenuItems.push({ label: 'Translations: Sync to Crowdin', action: 'syncTranslationsToCrowdin', }); } if (integration.excludedTargetLanguages) { crowdinMenuItems.push({ label: 'Change Files Target Languages', action: 'excludedTargetLanguages', }); } if (integration.forcePushTranslations) { crowdinMenuItems.push({ label: `Translations: Sync to ${appName} (Forced)`, action: 'forcePushTranslations', }); } return { integrationButtonMenuItems: integrationMenuItems.length > 0 ? JSON.stringify(integrationMenuItems) : null, crowdinButtonMenuItems: crowdinMenuItems.length > 0 ? JSON.stringify(crowdinMenuItems) : null, }; }