diff --git a/components/crowdin/.gitignore b/components/crowdin/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/crowdin/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/crowdin/actions/add-file/add-file.mjs b/components/crowdin/actions/add-file/add-file.mjs new file mode 100644 index 0000000000000..b5e7703dc6cd0 --- /dev/null +++ b/components/crowdin/actions/add-file/add-file.mjs @@ -0,0 +1,120 @@ +import fs from "fs"; +import { TYPE_OPTIONS } from "../../common/constants.mjs"; +import { + checkTmp, + parseObject, +} from "../../common/utils.mjs"; +import crowdin from "../../crowdin.app.mjs"; + +export default { + key: "crowdin-add-file", + name: "Add File to Project", + description: "Adds a file into the created project. [See the documentation](https://developer.crowdin.com/api/v2/#tag/source-files/operation/api.projects.files.post)", + version: "0.0.1", + type: "action", + props: { + crowdin, + projectId: { + propDefinition: [ + crowdin, + "projectId", + ], + }, + file: { + type: "string", + label: "File", + description: "The path to the file saved to the `/tmp` directory (e.g. `/tmp/example.jpg`) to process. [See the documentation](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory).", + }, + name: { + type: "string", + label: "Name", + description: "The name of the file in Crowdin. **Note:** Can't contain `\\ / : * ? \" < > |` symbols. `ZIP` files are not allowed.", + }, + branchId: { + propDefinition: [ + crowdin, + "branchId", + (c) => ({ + projectId: c.projectId, + }), + ], + }, + directoryId: { + propDefinition: [ + crowdin, + "directoryId", + (c) => ({ + projectId: c.projectId, + }), + ], + }, + title: { + type: "string", + label: "Title", + description: "Use to provide more details for translators. Title is available in UI only", + optional: true, + }, + context: { + type: "string", + label: "Context", + description: "Use to provide context about whole file", + optional: true, + }, + type: { + type: "string", + label: "File Type", + description: "The type of the file. **Note:** Use `docx` type to import each cell as a separate source string for XLSX file. Default is `auto`", + options: TYPE_OPTIONS, + optional: true, + }, + parserVersion: { + type: "integer", + label: "Parser Version", + description: "Using latest parser version by default. **Note:** Must be used together with `type`.", + optional: true, + }, + attachLabelIds: { + propDefinition: [ + crowdin, + "attachLabelIds", + (c) => ({ + projectId: c.projectId, + }), + ], + }, + }, + async run({ $ }) { + const { + crowdin, + attachLabelIds, + projectId, + file, + ...data + } = this; + + const fileBinary = fs.readFileSync(checkTmp(file)); + const crowdinFilename = file.startsWith("/tmp/") + ? file.slice(5) + : file; + + const fileResponse = await crowdin.createStorage({ + data: Buffer.from(fileBinary, "binary"), + headers: { + "Crowdin-API-FileName": encodeURI(crowdinFilename), + "Content-Type": "application/octet-stream", + }, + }); + + const response = await crowdin.uploadFileToProject({ + $, + projectId, + data: { + ...data, + storageId: fileResponse.data.id, + attachLabelIds: parseObject(attachLabelIds), + }, + }); + $.export("$summary", `Successfully uploaded file: ${this.name}`); + return response; + }, +}; diff --git a/components/crowdin/actions/create-project/create-project.mjs b/components/crowdin/actions/create-project/create-project.mjs new file mode 100644 index 0000000000000..004cd4499cb7f --- /dev/null +++ b/components/crowdin/actions/create-project/create-project.mjs @@ -0,0 +1,184 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { + LANGUAGE_ACCESS_POLICY_OPTIONS, + TAGS_DETECTION_OPTIONS, + VISIBILITY_OPTIONS, +} from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import crowdin from "../../crowdin.app.mjs"; + +export default { + key: "crowdin-create-project", + name: "Create Project", + description: "Creates a new project within Crowdin. [See the documentation](https://support.crowdin.com/developer/api/v2/#/projects-api/create-project)", + version: "0.0.1", + type: "action", + props: { + crowdin, + name: { + type: "string", + label: "Project Name", + description: "The name of the project to be created", + }, + sourceLanguageId: { + propDefinition: [ + crowdin, + "sourceLanguageId", + ], + }, + targetLanguageIds: { + propDefinition: [ + crowdin, + "sourceLanguageId", + ], + type: "string[]", + label: "Target Language IDs", + description: "Array of target language IDs", + optional: true, + }, + identifier: { + type: "string", + label: "Identifier", + description: "A custom identifier for the project", + optional: true, + }, + visibility: { + type: "string", + label: "Visibility", + description: "Defines how users can join the project.", + options: VISIBILITY_OPTIONS, + optional: true, + }, + languageAccessPolicy: { + type: "string", + label: "Language Access Policy", + description: "Defines access to project languages.", + optional: true, + options: LANGUAGE_ACCESS_POLICY_OPTIONS, + }, + cname: { + type: "string", + label: "Custom Domain Name", + description: "Custom domain name for the project.", + optional: true, + }, + description: { + type: "string", + label: "Project Description", + description: "The description of the project.", + optional: true, + }, + tagsDetection: { + type: "string", + label: "Tags Detection", + description: "The type of the tags detection.", + options: TAGS_DETECTION_OPTIONS, + optional: true, + }, + isMtAllowed: { + type: "boolean", + label: "Allow Machine Translation", + description: "Allows machine translations to be visible for translators. Default is **true**.", + optional: true, + }, + taskBasedAccessControl: { + type: "boolean", + label: "Task Based Access Control", + description: "Allow project members to work with tasks they're assigned to. Default is **false**.", + optional: true, + default: false, + }, + autoSubstitution: { + type: "boolean", + label: "Auto Substitution", + description: "Allows auto-substitution. Default is **true**.", + optional: true, + default: true, + }, + autoTranslateDialects: { + type: "boolean", + label: "Auto Translate Dialects", + description: "Automatically fill in regional dialects. Default is **false**.", + optional: true, + }, + publicDownloads: { + type: "boolean", + label: "Public Downloads", + description: "Allows translators to download source files. Default is **true**.", + optional: true, + }, + hiddenStringsProofreadersAccess: { + type: "boolean", + label: "Hidden Strings Proofreaders Access", + description: "Allows proofreaders to work with hidden strings. Default is **true**.", + optional: true, + default: true, + }, + useGlobalTm: { + type: "boolean", + label: "Use Global Translation Memory", + description: "If true, machine translations from connected MT engines will appear as suggestions. Default is **true**.", + optional: true, + }, + showTmSuggestionsDialects: { + type: "boolean", + label: "Show TM Suggestions for Dialects", + description: "Show primary language TM suggestions for dialects if there are no dialect-specific ones. Default is **true**.", + optional: true, + default: true, + }, + skipUntranslatedStrings: { + type: "boolean", + label: "Skip Untranslated Strings", + description: "Defines whether to skip untranslated strings.", + optional: true, + }, + exportApprovedOnly: { + type: "boolean", + label: "Export Approved Only", + description: "Defines whether to export only approved strings.", + optional: true, + }, + qaCheckIsActive: { + type: "boolean", + label: "QA Check Is Active", + description: "If true, QA checks are active. Default is **true**.", + optional: true, + }, + type: { + type: "string", + label: "Type", + description: "Defines the project type. To create a file-based project, use 0.", + options: [ + "0", + "1", + ], + optional: true, + }, + }, + async run({ $ }) { + try { + const { + crowdin, + type, + targetLanguageIds, + tagsDetection, + ...data + } = this; + + const response = await crowdin.createProject({ + $, + data: { + ...data, + type: parseInt(type), + targetLanguageIds: parseObject(targetLanguageIds), + tagsDetection: parseInt(tagsDetection), + }, + }); + $.export("$summary", `Project created successfully with Id: ${response.data.id}`); + return response; + } catch ({ response }) { + throw new ConfigurationError(response.data.errors[0]?.error?.errors[0]?.message); + } + }, +}; diff --git a/components/crowdin/actions/translate-via-machine-translation/translate-via-machine-translation.mjs b/components/crowdin/actions/translate-via-machine-translation/translate-via-machine-translation.mjs new file mode 100644 index 0000000000000..e6f46ed2157bc --- /dev/null +++ b/components/crowdin/actions/translate-via-machine-translation/translate-via-machine-translation.mjs @@ -0,0 +1,60 @@ +import { LANGUAGE_R_PROVIDER_OPTIONS } from "../../common/constants.mjs"; +import crowdin from "../../crowdin.app.mjs"; + +export default { + key: "crowdin-translate-via-machine-translation", + name: "Translate via Machine Translation", + description: "Performs machine translation of the uploaded files. [See the documentation](https://support.crowdin.com/developer/api/v2/)", + version: "0.0.1", + type: "action", + props: { + crowdin, + mtId: { + propDefinition: [ + crowdin, + "mtId", + ], + }, + targetLanguageId: { + propDefinition: [ + crowdin, + "sourceLanguageId", + ], + type: "string", + label: "Target Language ID", + description: "The language ID for the target translation language", + }, + languageRecognitionProvider: { + type: "string", + label: "Language Recognition Provider", + description: "Select a provider for language recognition **Note:** Is required if the source language is not selected", + options: LANGUAGE_R_PROVIDER_OPTIONS, + }, + sourceLanguageId: { + propDefinition: [ + crowdin, + "sourceLanguageId", + ], + }, + strings: { + type: "string[]", + label: "Strings", + description: "Array of strings to be translated. **Note:** You can translate up to 100 strings at a time.", + }, + }, + async run({ $ }) { + const response = await this.crowdin.performMachineTranslation({ + $, + mtId: this.mtId, + data: { + targetLanguageId: this.targetLanguageId, + strings: this.strings, + languageRecognitionProvider: this.languageRecognitionProvider, + sourceLanguageId: this.sourceLanguageId, + }, + }); + + $.export("$summary", "Successfully performed machine translation"); + return response; + }, +}; diff --git a/components/crowdin/app/crowdin.app.ts b/components/crowdin/app/crowdin.app.ts deleted file mode 100644 index 680d55b717589..0000000000000 --- a/components/crowdin/app/crowdin.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "crowdin", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); \ No newline at end of file diff --git a/components/crowdin/common/constants.mjs b/components/crowdin/common/constants.mjs new file mode 100644 index 0000000000000..21964b4eb113b --- /dev/null +++ b/components/crowdin/common/constants.mjs @@ -0,0 +1,266 @@ +export const LIMIT = 500; + +export const VISIBILITY_OPTIONS = [ + { + label: "Open - anyone can join the project", + value: "open", + }, + { + label: "Private - only invited users can join the project", + value: "private", + }, +]; + +export const LANGUAGE_ACCESS_POLICY_OPTIONS = [ + { + label: "Open - each project user can access all project languages", + value: "open", + }, + { + label: "Moderate - users should join each project language separately", + value: "moderate", + }, +]; + +export const TAGS_DETECTION_OPTIONS = [ + { + label: "Auto", + value: "0", + }, + { + label: "Count Tags", + value: "1", + }, + { + label: "Skip Tags", + value: "2", + }, +]; + +export const LANGUAGE_R_PROVIDER_OPTIONS = [ + "crowdin", + "engine", +]; + +export const TYPE_OPTIONS = [ + { + label: "Try to detect file type by extension or MIME type", + value: "auto", + }, + { + label: "Android (*.xml)", + value: "android", + }, + { + label: "Mac OS X / iOS (*.strings)", + value: "macosx", + }, + { + label: ".NET, Windows Phone (*.resx)", + value: "resx", + }, + { + label: "Java (*.properties)", + value: "properties", + }, + { + label: "GNU GetText (*.po, *.pot)", + value: "gettext", + }, + { + label: "Ruby On Rails (*.yaml, *.yml)", + value: "yaml", + }, + { + label: "Hypertext Preprocessor (*.php)", + value: "php", + }, + { + label: "Generic JSON (*.json)", + value: "json", + }, + { + label: "Generic XML (*.xml)", + value: "xml", + }, + { + label: "Generic INI (*.ini)", + value: "ini", + }, + { + label: "Windows Resources (*.rc)", + value: "rc", + }, + { + label: "Windows 8 Metro (*.resw)", + value: "resw", + }, + { + label: "Windows 8 Metro (*.resjson)", + value: "resjson", + }, + { + label: "Nokia Qt (*.ts)", + value: "qtts", + }, + { + label: "Joomla localizable resources (*.ini)", + value: "joomla", + }, + { + label: "Google Chrome Extension (*.json)", + value: "chrome", + }, + { + label: "Mozilla DTD (*.dtd)", + value: "dtd", + }, + { + label: "Delphi DKLang (*.dklang)", + value: "dklang", + }, + { + label: "Flex (*.properties)", + value: "flex", + }, + { + label: "NSIS Installer Resources (*.nsh)", + value: "nsh", + }, + { + label: "WiX Installer (*.wxl)", + value: "wxl", + }, + { + label: "XLIFF (*.xliff, *.xlf)", + value: "xliff", + }, + { + label: "XLIFF 2.0 (*.xliff, *.xlf)", + value: "xliff_two", + }, + { + label: "HTML (*.html, *.htm, *.xhtml, *.xhtm, *.xht, *.hbs, *.liquid)", + value: "html", + }, + { + label: "Haml (*.haml)", + value: "haml", + }, + { + label: "Plain Text (*.txt)", + value: "txt", + }, + { + label: "Comma Separated Values (*.csv)", + value: "csv", + }, + { + label: "Markdown (*.md, *.text, *.markdown...)", + value: "md", + }, + { + label: "MadCap Flare (*.flnsp, .flpgpl .fltoc)", + value: "flsnp", + }, + { + label: "Jekyll HTML (*.html)", + value: "fm_html", + }, + { + label: "Jekyll Markdown (*.md)", + value: "fm_md", + }, + { + label: "MediaWiki (*.wiki, *.wikitext, *.mediawiki)", + value: "mediawiki", + }, + { + label: "Microsoft Office, OpenOffice.org Documents, Adobe InDesign, Adobe FrameMaker(*.docx, *.dotx, *.docm, *.dotm, *.xlsx, *.xltx, *.xlsm, *.xltm, *.pptx, *.potx, *.ppsx, *.pptm, *.potm, *.ppsm, *.odt, *.ods, *.odg, *.odp, *.imdl, *.mif)", + value: "docx", + }, + { + label: "Microsoft Excel (*.xlsx)", + value: "xlsx", + }, + { + label: "Youtube .sbv (*.sbv)", + value: "sbv", + }, + { + label: "Play Framework", + value: "properties_play", + }, + { + label: "Java Application (*.xml)", + value: "properties_xml", + }, + { + label: "Maxthon Browser (*.ini)", + value: "maxthon", + }, + { + label: "Go (*.gotext.json)", + value: "go_json", + }, + { + label: "DITA Document (*.dita, *.ditamap)", + value: "dita", + }, + { + label: "Adobe FrameMaker (*.mif)", + value: "mif", + }, + { + label: "Adobe InDesign (*.idml)", + value: "idml", + }, + { + label: "iOS (*.stringsdict)", + value: "stringsdict", + }, + { + label: "Mac OS property list (*.plist)", + value: "plist", + }, + { + label: "Video Subtitling and WebVTT (*.vtt)", + value: "vtt", + }, + { + label: "Steamworks Localization Valve Data File (*.vdf)", + value: "vdf", + }, + { + label: "SubRip .srt (*.srt)", + value: "srt", + }, + { + label: "Salesforce (*.stf)", + value: "stf", + }, + { + label: "Toml (*.toml)", + value: "toml", + }, + { + label: "Contentful (*.json)", + value: "contentful_rt", + }, + { + label: "SVG (*.svg)", + value: "svg", + }, + { + label: "JavaScript (*.js)", + value: "js", + }, + { + label: "CoffeeScript (*.coffee)", + value: "coffee", + }, + { + label: "NestJS i18n", + value: "nestjs_i18n", + }, +]; diff --git a/components/crowdin/common/utils.mjs b/components/crowdin/common/utils.mjs new file mode 100644 index 0000000000000..32b6f5a875df7 --- /dev/null +++ b/components/crowdin/common/utils.mjs @@ -0,0 +1,31 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; + +export const checkTmp = (filename) => { + if (filename.indexOf("/tmp") === -1) { + return `/tmp/${filename}`; + } + return filename; +}; diff --git a/components/crowdin/crowdin.app.mjs b/components/crowdin/crowdin.app.mjs new file mode 100644 index 0000000000000..2829d52aa45d9 --- /dev/null +++ b/components/crowdin/crowdin.app.mjs @@ -0,0 +1,306 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + +export default { + type: "app", + app: "crowdin", + propDefinitions: { + projectId: { + type: "string", + label: "Project ID", + description: "The ID of the project", + async options({ page }) { + const { data } = await this.listProjects({ + params: { + limit: LIMIT, + offset: LIMIT * page, + }, + }); + return data.map(({ + data: { + id: value, name: label, + }, + }) => ({ + label, + value, + })); + }, + }, + directoryId: { + type: "string", + label: "Directory ID", + description: "The ID of the directory. **Note:** Can't be used with `Branch Id` in same request", + optional: true, + async options({ + page, projectId, + }) { + const { data } = await this.listDirectories({ + projectId, + params: { + limit: LIMIT, + offset: LIMIT * page, + }, + }); + return data.map(({ + data: { + id: value, name: label, + }, + }) => ({ + label, + value, + })); + }, + }, + sourceLanguageId: { + type: "string", + label: "Source Language ID", + description: "The language ID of the source language", + async options({ page }) { + const { data } = await this.listSupportedLanguages({ + params: { + limit: LIMIT, + offset: LIMIT * page, + }, + }); + return data.map(({ + data: { + id: value, name: label, + }, + }) => ({ + label, + value, + })); + }, + }, + storageId: { + type: "integer", + label: "Storage ID", + description: "The ID of the storage", + async options({ page }) { + const { data } = await this.listStorages({ + params: { + limit: LIMIT, + offset: LIMIT * page, + }, + }); + return data.map(({ + data: { + id: value, fileName: label, + }, + }) => ({ + label, + value, + })); + }, + }, + branchId: { + type: "string", + label: "Branch ID", + description: "Defines branch to which file will be added. **Note:** Can't be used with `Directory Id` in same request", + optional: true, + async options({ + page, projectId, + }) { + const { data } = await this.listBranches({ + projectId, + params: { + limit: LIMIT, + offset: LIMIT * page, + }, + }); + return data.map(({ + data: { + id: value, name, title, + }, + }) => ({ + label: `${title} - ${name}`, + value, + })); + }, + }, + attachLabelIds: { + type: "string[]", + label: "Attach Label IDs", + description: "The IDs of the labels to attach", + optional: true, + async options({ + page, projectId, + }) { + const { data } = await this.listLabels({ + projectId, + params: { + limit: LIMIT, + offset: LIMIT * page, + }, + }); + return data.map(({ + data: { + id: value, title: label, + }, + }) => ({ + label, + value, + })); + }, + }, + mtId: { + type: "string", + label: "Machine Translation ID", + description: "The ID of the machine translation engine", + async options({ page }) { + const { data } = await this.listMTs({ + params: { + limit: LIMIT, + offset: LIMIT * page, + }, + }); + return data.map(({ + data: { + id: value, name: label, + }, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.crowdin.com/api/v2"; + }, + _headers(headers = {}) { + return { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + ...headers, + }; + }, + _makeRequest({ + $ = this, path, headers, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(headers), + ...opts, + }); + }, + listProjects(opts = {}) { + return this._makeRequest({ + path: "/projects", + ...opts, + }); + }, + listDirectories({ + projectId, ...opts + }) { + return this._makeRequest({ + path: `/projects/${projectId}/directories`, + ...opts, + }); + }, + listSupportedLanguages(opts = {}) { + return this._makeRequest({ + path: "/languages", + ...opts, + }); + }, + listStorages(opts = {}) { + return this._makeRequest({ + path: "/storages", + ...opts, + }); + }, + listBranches({ + projectId, ...opts + }) { + return this._makeRequest({ + path: `/projects/${projectId}/branches`, + ...opts, + }); + }, + listLabels({ + projectId, ...opts + }) { + return this._makeRequest({ + path: `/projects/${projectId}/labels`, + ...opts, + }); + }, + listMTs(opts = {}) { + return this._makeRequest({ + path: "/mts", + ...opts, + }); + }, + createProject(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/projects", + ...opts, + }); + }, + createStorage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/storages", + ...opts, + }); + }, + uploadFileToProject({ + projectId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/projects/${projectId}/files`, + ...opts, + }); + }, + performMachineTranslation({ + mtId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/mts/${mtId}/translations`, + ...opts, + }); + }, + createWebhook({ + projectId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/projects/${projectId}/webhooks`, + ...opts, + }); + }, + deleteWebhook({ + projectId, webhookId, + }) { + return this._makeRequest({ + method: "DELETE", + path: `/projects/${projectId}/webhooks/${webhookId}`, + }); + }, + async *paginate({ + fn, params = {}, ...opts + }) { + let hasMore = false; + let page = 0; + + do { + params.limit = LIMIT; + params.offset = LIMIT * page++; + const { data } = await fn({ + params, + ...opts, + }); + for (const d of data) { + yield d; + } + + hasMore = data.length; + + } while (hasMore); + }, + }, +}; diff --git a/components/crowdin/package.json b/components/crowdin/package.json index 33034ea5d16a9..2b917337bf538 100644 --- a/components/crowdin/package.json +++ b/components/crowdin/package.json @@ -1,16 +1,19 @@ { "name": "@pipedream/crowdin", - "version": "0.0.2", + "version": "0.1.0", "description": "Pipedream Crowdin Components", - "main": "dist/app/crowdin.app.mjs", + "main": "crowdin.app.mjs", "keywords": [ "pipedream", "crowdin" ], - "files": ["dist"], "homepage": "https://pipedream.com/apps/crowdin", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3", + "fs": "^0.0.1-security" } } diff --git a/components/crowdin/sources/common/base-instant.mjs b/components/crowdin/sources/common/base-instant.mjs new file mode 100644 index 0000000000000..13341ebf7cc50 --- /dev/null +++ b/components/crowdin/sources/common/base-instant.mjs @@ -0,0 +1,68 @@ +import crowdin from "../../crowdin.app.mjs"; + +export default { + props: { + crowdin, + http: { + type: "$.interface.http", + customResponse: true, + }, + db: "$.service.db", + name: { + type: "string", + label: "Name", + description: "The webhook name.", + }, + projectId: { + propDefinition: [ + crowdin, + "projectId", + ], + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + getExtraData() { + return {}; + }, + }, + hooks: { + async activate() { + const response = await this.crowdin.createWebhook({ + projectId: this.projectId, + data: { + name: this.name, + url: this.http.endpoint, + events: this.getEvents(), + requestType: "POST", + }, + }); + this._setHookId(response.data.id); + }, + async deactivate() { + const webhookId = this._getHookId(); + await this.crowdin.deleteWebhook({ + projectId: this.projectId, + webhookId, + }); + }, + }, + async run({ body }) { + + this.http.respond({ + status: 200, + }); + + const ts = Date.parse(new Date()); + this.$emit(body, { + id: `${body.comment?.id || ts}`, + summary: this.getSummary(body), + ts: ts, + }); + }, +}; diff --git a/components/crowdin/sources/common/base.mjs b/components/crowdin/sources/common/base.mjs new file mode 100644 index 0000000000000..9709d575c5d06 --- /dev/null +++ b/components/crowdin/sources/common/base.mjs @@ -0,0 +1,66 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import crowdin from "../../crowdin.app.mjs"; + +export default { + props: { + crowdin, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + projectId: { + propDefinition: [ + crowdin, + "projectId", + ], + }, + }, + methods: { + _getLastId() { + return this.db.get("lastId") || 0; + }, + _setLastId(lastId) { + this.db.set("lastId", lastId); + }, + async emitEvent(maxResults = false) { + const lastId = this._getLastId(); + + const response = this.crowdin.paginate({ + projectId: this.projectId, + ...this.getArgs(), + }); + + let responseArray = []; + for await (const item of response) { + if (item.data.id <= lastId) break; + responseArray.push(item.data); + } + + if (responseArray.length) { + if (maxResults && (responseArray.length > maxResults)) { + responseArray.length = maxResults; + } + this._setLastId(responseArray[0].id); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item.id, + summary: this.getSummary(item), + ts: Date.parse(item.createdAt), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/crowdin/sources/file-approved-instant/file-approved-instant.mjs b/components/crowdin/sources/file-approved-instant/file-approved-instant.mjs new file mode 100644 index 0000000000000..1672800d09e5a --- /dev/null +++ b/components/crowdin/sources/file-approved-instant/file-approved-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base-instant.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "crowdin-file-approved-instant", + name: "New File Approved (Instant)", + description: "Emit new event when a file is fully translated and approved.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return [ + "project.approved", + ]; + }, + getSummary(body) { + return `File approved for project ID: ${body.project.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/crowdin/sources/file-approved-instant/test-event.mjs b/components/crowdin/sources/file-approved-instant/test-event.mjs new file mode 100644 index 0000000000000..bc81fc2ff5f3e --- /dev/null +++ b/components/crowdin/sources/file-approved-instant/test-event.mjs @@ -0,0 +1,36 @@ +export default { + "event": "project.approved", + "project": { + "id": "123123", + "userId": "123123", + "sourceLanguageId": "en", + "targetLanguageIds": [ + "ru", + "pt-BR" + ], + "identifier": "project-01", + "name": "Project 01", + "createdAt": "2024-10-28T13:54:03+00:00", + "updatedAt": "2024-10-30T14:59:43+00:00", + "lastActivity": "2024-10-30T16:35:02+00:00", + "description": "", + "url": "https://crowdin.com/project/project-01", + "cname": null, + "languageAccessPolicy": "open", + "visibility": "private", + "publicDownloads": true + }, + "targetLanguage": { + "id": "en", + "name": "English", + "editorCode": "en", + "twoLettersCode": "en", + "threeLettersCode": "eng", + "locale": "en", + "androidCode": "en", + "osxCode": "en.lproj", + "osxLocale": "en", + "textDirection": "ltr", + "dialectOf": null + } +} \ No newline at end of file diff --git a/components/crowdin/sources/new-comment-issue-instant/new-comment-issue-instant.mjs b/components/crowdin/sources/new-comment-issue-instant/new-comment-issue-instant.mjs new file mode 100644 index 0000000000000..17098ecb6a80d --- /dev/null +++ b/components/crowdin/sources/new-comment-issue-instant/new-comment-issue-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base-instant.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "crowdin-new-comment-issue-instant", + name: "New Comment or Issue Added (Instant)", + description: "Emit new event when a user adds a comment or an issue in Crowdin.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return [ + "stringComment.created", + ]; + }, + getSummary(body) { + return `New comment or issue in project: ${body.comment.string.project.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/crowdin/sources/new-comment-issue-instant/test-event.mjs b/components/crowdin/sources/new-comment-issue-instant/test-event.mjs new file mode 100644 index 0000000000000..18737adfb28e4 --- /dev/null +++ b/components/crowdin/sources/new-comment-issue-instant/test-event.mjs @@ -0,0 +1,91 @@ +export default { + "event": "stringComment.created", + "comment": { + "id": "4", + "text": "text 01", + "type": "issue", + "issueType": "translation_mistake", + "issueStatus": "unresolved", + "resolvedAt": null, + "createdAt": "2024-10-30T17:06:06+00:00", + "string": { + "id": "2", + "identifier": "5ad6c6099c2bc7211eb8151483826e80", + "key": "NFDBB2FA9", + "text": "Text file", + "type": "text", + "context": "NFDBB2FA9", + "maxLength": "0", + "isHidden": false, + "isDuplicate": false, + "masterStringId": null, + "revision": "1", + "hasPlurals": false, + "labelIds": [], + "url": "https://crowdin.com/editor/project-01/14/en-ptbr#2", + "createdAt": "2024-10-28T14:18:30+00:00", + "updatedAt": null, + "file": { + "id": "14", + "name": "file.docx", + "title": null, + "type": "docx13", + "path": "/Folder 01/dummy.docx", + "status": "active", + "revision": "1", + "branchId": null, + "directoryId": "2" + }, + "project": { + "id": "123123", + "userId": "123123", + "sourceLanguageId": "en", + "targetLanguageIds": [ + "ru", + "pt-BR", + "yi", + "am", + "es-PR", + "es-NI", + "ar-YE" + ], + "identifier": "project-01", + "name": "Project 01", + "createdAt": "2024-10-28T13:54:03+00:00", + "updatedAt": "2024-10-30T16:40:17+00:00", + "lastActivity": "2024-10-30T17:03:02+00:00", + "description": "", + "url": "https://crowdin.com/project/project-01", + "cname": null, + "languageAccessPolicy": "open", + "visibility": "private", + "publicDownloads": true + } + }, + "targetLanguage": { + "id": "pt-BR", + "name": "Portuguese, Brazilian", + "editorCode": "ptbr", + "twoLettersCode": "pt", + "threeLettersCode": "por", + "locale": "pt-BR", + "androidCode": "pt-rBR", + "osxCode": "pt-BR.lproj", + "osxLocale": "pt_BR", + "textDirection": "ltr", + "dialectOf": null + }, + "user": { + "id": "16645627", + "username": "username", + "fullName": "username", + "avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/123123/small/f1fb36bf9de3ded05b455e88ac9c77cf_default.png" + }, + "commentResolver": { + "id": null, + "username": null, + "fullName": null, + "avatarUrl": null + } + } +} \ No newline at end of file diff --git a/components/crowdin/sources/new-directory/new-directory.mjs b/components/crowdin/sources/new-directory/new-directory.mjs new file mode 100644 index 0000000000000..63a5600c94051 --- /dev/null +++ b/components/crowdin/sources/new-directory/new-directory.mjs @@ -0,0 +1,27 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "crowdin-new-directory", + name: "New Directory Created", + description: "Emit new event when a new directory is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getArgs() { + return { + fn: this.crowdin.listDirectories, + params: { + orderBy: "createdAt desc", + }, + }; + }, + getSummary(item) { + return `New Directory: ${item.name}`; + }, + }, + sampleEmit, +}; diff --git a/components/crowdin/sources/new-directory/test-event.mjs b/components/crowdin/sources/new-directory/test-event.mjs new file mode 100644 index 0000000000000..9b19e5716e62c --- /dev/null +++ b/components/crowdin/sources/new-directory/test-event.mjs @@ -0,0 +1,15 @@ +export default { + "data": { + "id": 4, + "projectId": 2, + "branchId": 34, + "directoryId": null, + "name": "main", + "title": "", + "exportPattern": "/localization/%locale%/%file_name%", + "path": "/main", + "priority": "normal", + "createdAt": "2019-09-19T14:14:00+00:00", + "updatedAt": "2019-09-19T14:14:00+00:00" + } +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d5dd23b96b0b..337ad5363ce4d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2234,7 +2234,12 @@ importers: specifiers: {} components/crowdin: - specifiers: {} + specifiers: + '@pipedream/platform': ^3.0.3 + fs: ^0.0.1-security + dependencies: + '@pipedream/platform': 3.0.3 + fs: 0.0.1-security components/crowdpower: specifiers: {} @@ -13192,55 +13197,6 @@ packages: - aws-crt dev: false - /@aws-sdk/client-sso-oidc/3.600.0_tdq3komn4zwyd65w7klbptsu34: - resolution: {integrity: sha512-7+I8RWURGfzvChyNQSyj5/tKrqRbzRl7H+BnTOf/4Vsw1nFOi5ROhlhD4X/Y0QCTacxnaoNcIrqnY7uGGvVRzw==} - engines: {node: '>=16.0.0'} - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/client-sts': 3.600.0 - '@aws-sdk/core': 3.598.0 - '@aws-sdk/credential-provider-node': 3.600.0_f7n47caigsrjd2lr2szmwfuee4 - '@aws-sdk/middleware-host-header': 3.598.0 - '@aws-sdk/middleware-logger': 3.598.0 - '@aws-sdk/middleware-recursion-detection': 3.598.0 - '@aws-sdk/middleware-user-agent': 3.598.0 - '@aws-sdk/region-config-resolver': 3.598.0 - '@aws-sdk/types': 3.598.0 - '@aws-sdk/util-endpoints': 3.598.0 - '@aws-sdk/util-user-agent-browser': 3.598.0 - '@aws-sdk/util-user-agent-node': 3.598.0 - '@smithy/config-resolver': 3.0.3 - '@smithy/core': 2.2.3 - '@smithy/fetch-http-handler': 3.2.1 - '@smithy/hash-node': 3.0.2 - '@smithy/invalid-dependency': 3.0.2 - '@smithy/middleware-content-length': 3.0.2 - '@smithy/middleware-endpoint': 3.0.4 - '@smithy/middleware-retry': 3.0.6 - '@smithy/middleware-serde': 3.0.3 - '@smithy/middleware-stack': 3.0.3 - '@smithy/node-config-provider': 3.1.3 - '@smithy/node-http-handler': 3.1.2 - '@smithy/protocol-http': 4.0.3 - '@smithy/smithy-client': 3.1.6 - '@smithy/types': 3.3.0 - '@smithy/url-parser': 3.0.3 - '@smithy/util-base64': 3.0.0 - '@smithy/util-body-length-browser': 3.0.0 - '@smithy/util-body-length-node': 3.0.0 - '@smithy/util-defaults-mode-browser': 3.0.6 - '@smithy/util-defaults-mode-node': 3.0.6 - '@smithy/util-endpoints': 2.0.3 - '@smithy/util-middleware': 3.0.3 - '@smithy/util-retry': 3.0.2 - '@smithy/util-utf8': 3.0.0 - tslib: 2.6.3 - transitivePeerDependencies: - - '@aws-sdk/client-sts' - - aws-crt - dev: false - /@aws-sdk/client-sso/3.423.0: resolution: {integrity: sha512-znIufHkwhCIePgaYciIs3x/+BpzR57CZzbCKHR9+oOvGyufEPPpUT5bFLvbwTgfiVkTjuk6sG/ES3U5Bc+xtrA==} engines: {node: '>=14.0.0'} @@ -13476,7 +13432,55 @@ packages: dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/client-sso-oidc': 3.600.0_tdq3komn4zwyd65w7klbptsu34 + '@aws-sdk/client-sso-oidc': 3.600.0 + '@aws-sdk/core': 3.598.0 + '@aws-sdk/credential-provider-node': 3.600.0_f7n47caigsrjd2lr2szmwfuee4 + '@aws-sdk/middleware-host-header': 3.598.0 + '@aws-sdk/middleware-logger': 3.598.0 + '@aws-sdk/middleware-recursion-detection': 3.598.0 + '@aws-sdk/middleware-user-agent': 3.598.0 + '@aws-sdk/region-config-resolver': 3.598.0 + '@aws-sdk/types': 3.598.0 + '@aws-sdk/util-endpoints': 3.598.0 + '@aws-sdk/util-user-agent-browser': 3.598.0 + '@aws-sdk/util-user-agent-node': 3.598.0 + '@smithy/config-resolver': 3.0.3 + '@smithy/core': 2.2.3 + '@smithy/fetch-http-handler': 3.2.1 + '@smithy/hash-node': 3.0.2 + '@smithy/invalid-dependency': 3.0.2 + '@smithy/middleware-content-length': 3.0.2 + '@smithy/middleware-endpoint': 3.0.4 + '@smithy/middleware-retry': 3.0.6 + '@smithy/middleware-serde': 3.0.3 + '@smithy/middleware-stack': 3.0.3 + '@smithy/node-config-provider': 3.1.3 + '@smithy/node-http-handler': 3.1.2 + '@smithy/protocol-http': 4.0.3 + '@smithy/smithy-client': 3.1.6 + '@smithy/types': 3.3.0 + '@smithy/url-parser': 3.0.3 + '@smithy/util-base64': 3.0.0 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-body-length-node': 3.0.0 + '@smithy/util-defaults-mode-browser': 3.0.6 + '@smithy/util-defaults-mode-node': 3.0.6 + '@smithy/util-endpoints': 2.0.3 + '@smithy/util-middleware': 3.0.3 + '@smithy/util-retry': 3.0.2 + '@smithy/util-utf8': 3.0.0 + tslib: 2.6.3 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-sts/3.600.0_dseaa2p5u2yk67qiepewcq3hkq: + resolution: {integrity: sha512-KQG97B7LvTtTiGmjlrG1LRAY8wUvCQzrmZVV5bjrJ/1oXAU7DITYwVbSJeX9NWg6hDuSk0VE3MFwIXS2SvfLIA==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/client-sso-oidc': 3.600.0 '@aws-sdk/core': 3.598.0 '@aws-sdk/credential-provider-node': 3.600.0_f7n47caigsrjd2lr2szmwfuee4 '@aws-sdk/middleware-host-header': 3.598.0 @@ -13515,6 +13519,7 @@ packages: '@smithy/util-utf8': 3.0.0 tslib: 2.6.3 transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' - aws-crt dev: false @@ -17810,7 +17815,7 @@ packages: '@aws-sdk/client-sns': 3.423.0 '@aws-sdk/client-sqs': 3.423.0 '@aws-sdk/client-ssm': 3.423.0 - '@aws-sdk/client-sts': 3.600.0 + '@aws-sdk/client-sts': 3.600.0_dseaa2p5u2yk67qiepewcq3hkq '@aws-sdk/s3-request-presigner': 3.609.0 '@pipedream/helper_functions': 0.3.12 '@pipedream/platform': 1.6.6