diff --git a/.github/workflows/check-i18n-task.yml b/.github/workflows/check-i18n-task.yml
new file mode 100644
index 000000000..121a9a844
--- /dev/null
+++ b/.github/workflows/check-i18n-task.yml
@@ -0,0 +1,38 @@
+name: Check Internationalization
+
+# See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows
+on:
+ push:
+ paths:
+ - '.github/workflows/check-i18n-task.ya?ml'
+ - '**/package.json'
+ - '**.ts'
+ - 'i18n/**'
+ pull_request:
+ paths:
+ - '.github/workflows/check-i18n-task.ya?ml'
+ - '**/package.json'
+ - '**.ts'
+ - 'i18n/**'
+ workflow_dispatch:
+ repository_dispatch:
+
+jobs:
+ check:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+
+ - name: Install Node.js 12.x
+ uses: actions/setup-node@v2
+ with:
+ node-version: '12.14.1'
+ registry-url: 'https://registry.npmjs.org'
+
+ - name: Install dependencies
+ run: yarn
+
+ - name: Check for errors
+ run: yarn i18n:check
diff --git a/.github/workflows/i18n-nightly-push.yml b/.github/workflows/i18n-nightly-push.yml
new file mode 100644
index 000000000..c62f16a7f
--- /dev/null
+++ b/.github/workflows/i18n-nightly-push.yml
@@ -0,0 +1,30 @@
+name: i18n-nightly-push
+
+on:
+ schedule:
+ # run every day at 1AM
+ - cron: '0 1 * * *'
+
+jobs:
+ push-to-transifex:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Install Node.js 12.x
+ uses: actions/setup-node@v2
+ with:
+ node-version: '12.14.1'
+ registry-url: 'https://registry.npmjs.org'
+
+ - name: Install dependencies
+ run: yarn
+
+ - name: Run i18n:push script
+ run: yarn run i18n:push
+ env:
+ TRANSIFEX_ORGANIZATION: ${{ secrets.TRANSIFEX_ORGANIZATION }}
+ TRANSIFEX_PROJECT: ${{ secrets.TRANSIFEX_PROJECT }}
+ TRANSIFEX_RESOURCE: ${{ secrets.TRANSIFEX_RESOURCE }}
+ TRANSIFEX_API_KEY: ${{ secrets.TRANSIFEX_API_KEY }}
diff --git a/.github/workflows/i18n-weekly-pull.yml b/.github/workflows/i18n-weekly-pull.yml
new file mode 100644
index 000000000..1a361febe
--- /dev/null
+++ b/.github/workflows/i18n-weekly-pull.yml
@@ -0,0 +1,38 @@
+name: i18n-weekly-pull
+
+on:
+ schedule:
+ # run every monday at 2AM
+ - cron: '0 2 * * 1'
+
+jobs:
+ pull-from-transifex:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Install Node.js 12.x
+ uses: actions/setup-node@v2
+ with:
+ node-version: '12.14.1'
+ registry-url: 'https://registry.npmjs.org'
+
+ - name: Install dependencies
+ run: yarn
+
+ - name: Run i18n:pull script
+ run: yarn run i18n:pull
+ env:
+ TRANSIFEX_ORGANIZATION: ${{ secrets.TRANSIFEX_ORGANIZATION }}
+ TRANSIFEX_PROJECT: ${{ secrets.TRANSIFEX_PROJECT }}
+ TRANSIFEX_RESOURCE: ${{ secrets.TRANSIFEX_RESOURCE }}
+ TRANSIFEX_API_KEY: ${{ secrets.TRANSIFEX_API_KEY }}
+
+ - name: Create Pull Request
+ uses: peter-evans/create-pull-request@v3
+ with:
+ commit-message: Updated translation files
+ title: Update translation files
+ branch: i18n/translations-update
+ author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
diff --git a/i18n/en.json b/i18n/en.json
new file mode 100644
index 000000000..77201b48c
--- /dev/null
+++ b/i18n/en.json
@@ -0,0 +1,221 @@
+{
+ "arduino": {
+ "common": {
+ "selectBoard": "Select Board",
+ "unknown": "Unknown",
+ "processing": "Processing",
+ "saveChangesToSketch": "Do you want to save changes to this sketch before closing?",
+ "loseChanges": "If you don't save, your changes will be lost.",
+ "noBoardSelected": "No board selected"
+ },
+ "preferences": {
+ "language.log": "True if the Arduino Language Server should generate log files into the sketch folder. Otherwise, false. It's false by default.",
+ "compile.verbose": "True for verbose compile output. False by default",
+ "compile.warnings": "Tells gcc which warning level to use. It's 'None' by default",
+ "upload.verbose": "True for verbose upload output. False by default.",
+ "window.autoScale": "True if the user interface automatically scales with the font size.",
+ "window.zoomLevel": "Adjust the zoom level of the window. The original size is 0 and each increment above (e.g. 1) or below (e.g. -1) represents zooming 20% larger or smaller. You can also enter decimals to adjust the zoom level with a finer granularity.",
+ "ide.autoUpdate": "True to enable automatic update checks. The IDE will check for updates automatically and periodically.",
+ "board.certificates": "List of certificates that can be uploaded to boards",
+ "sketchbook.showAllFiles": "True to show all sketch files inside the sketch. It is false by default.",
+ "cloud.enabled": "True if the sketch sync functions are enabled. Defaults to true.",
+ "cloud.pull.warn": "True if users should be warned before pulling a cloud sketch. Defaults to true.",
+ "cloud.push.warn": "True if users should be warned before pushing a cloud sketch. Defaults to true.",
+ "cloud.pushpublic.warn": "True if users should be warned before pushing a public sketch to the cloud. Defaults to true.",
+ "cloud.sketchSyncEnpoint": "The endpoint used to push and pull sketches from a backend. By default it points to Arduino Cloud API.",
+ "auth.clientID": "The OAuth2 client ID.",
+ "auth.domain": "The OAuth2 domain.",
+ "auth.audience": "The OAuth2 audience.",
+ "auth.registerUri": "The URI used to register a new user."
+ },
+ "cloud": {
+ "signIn": "Sign in",
+ "signOut": "Sign Out",
+ "shareSketch": "Share Sketch",
+ "showHideRemoveSketchbook": "Show/Hide Remote Sketchbook",
+ "pullSketch": "Pull Sketch",
+ "openInCloudEditor": "Open in Cloud Editor",
+ "options": "Options...",
+ "share": "Share...",
+ "remote": "Remote",
+ "continue": "Continue",
+ "pushSketch": "Push Sketch",
+ "pushSketchMsg": "This is a Public Sketch. Before pushing, make sure any sensitive information is defined in arduino_secrets.h files. You can make a Sketch private from the Share panel.",
+ "pull": "Pull",
+ "pullSketchMsg": "Pulling this Sketch from the Cloud will overwrite its local version. Are you sure you want to continue?",
+ "donePulling": "Done pulling ‘{0}’.",
+ "notYetPulled": "Cannot push to Cloud. It is not yet pulled.",
+ "push": "Push",
+ "pullFirst": "You have to pull first to be able to push to the Cloud.",
+ "donePushing": "Done pushing ‘{0}’."
+ },
+ "board": {
+ "installManually": "Install Manually",
+ "installNow": "The \"{0} {1}\" core has to be installed for the currently selected \"{2}\" board. Do you want to install it now?",
+ "configDialogTitle": "Select Other Board & Port",
+ "configDialog1": "Select both a Board and a Port if you want to upload a sketch.",
+ "configDialog2": "If you only select a Board you will be able just to compile, but not to upload your sketch.",
+ "pleasePickBoard": "Please pick a board connected to the port you have selected.",
+ "programmer": "Programmer",
+ "succesfullyInstalledPlatform": "Successfully installed platform {0}:{1}",
+ "succesfullyUninstalledPlatform": "Successfully uninstalled platform {0}:{1}",
+ "couldNotFindPreviouslySelected": "Could not find previously selected board '{0}' in installed platform '{1}'. Please manually reselect the board you want to use. Do you want to reselect it now?",
+ "reselectLater": "Reselect later",
+ "noneSelected": "No boards selected.",
+ "noPortsSelected": "No ports selected for board: '{0}'.",
+ "noFQBN": "The FQBN is not available for the selected board \"{0}\". Do you have the corresponding core installed?",
+ "selectBoardForInfo": "Please select a board to obtain board info.",
+ "platformMissing": "The platform for the selected '{0}' board is not installed.",
+ "selectPortForInfo": "Please select a port to obtain board info.",
+ "boardInfo": "Board Info",
+ "board": "Board{0}",
+ "port": "Port{0}",
+ "getBoardInfo": "Get Board Info",
+ "inSketchbook": " (in Sketchbook)"
+ },
+ "boardsManager": "Boards Manager",
+ "about": {
+ "label": "About {0}",
+ "detail": "Version: {0}\nDate: {1}{2}\nCLI Version: {3}{4} [{5}]\n\n{6}"
+ },
+ "contributions": {
+ "addFile": "Add File",
+ "replaceTitle": "Replace",
+ "fileAdded": "One file added to the sketch."
+ },
+ "replaceMsg": "Replace the existing version of {0}?",
+ "library": {
+ "addZip": "Add .ZIP Library...",
+ "zipLibrary": "Library",
+ "overwriteExistingLibrary": "Do you want to overwrite the existing library?",
+ "successfullyInstalledZipLibrary": "Successfully installed library from {0} archive",
+ "namedLibraryAlreadyExists": "A library folder named {0} already exists. Do you want to overwrite it?",
+ "libraryAlreadyExists": "A library already exists. Do you want to overwrite it?",
+ "include": "Include Library",
+ "manageLibraries": "Manage Libraries...",
+ "arduinoLibraries": "Arduino libraries",
+ "contributedLibraries": "Contributed libraries",
+ "title": "Library Manager",
+ "needsOneDependency": "The library {0}:{1} needs another dependency currently not installed:",
+ "needsMultipleDependencies": "The library {0}:{1} needs some other dependencies currently not installed:",
+ "installOneMissingDependency": "Would you like to install the missing dependency?",
+ "installMissingDependencies": "Would you like to install all the missing dependencies?",
+ "dependenciesForLibrary": "Dependencies for library {0}:{1}",
+ "installAll": "Install all",
+ "installOnly": "Install {0} only",
+ "installedSuccessfully": "Successfully installed library {0}:{1}",
+ "uninstalledSuccessfully": "Successfully uninstalled library {0}:{1}"
+ },
+ "selectZip": "Select a zip file containing the library you'd like to add",
+ "sketch": {
+ "archiveSketch": "Archive Sketch",
+ "saveSketchAs": "Save sketch folder as...",
+ "createdArchive": "Created archive '{0}'.",
+ "new": "New",
+ "openRecent": "Open Recent",
+ "showFolder": "Show Sketch Folder",
+ "sketch": "Sketch",
+ "moving": "Moving",
+ "movingMsg": "The file \"{0}\" needs to be inside a sketch folder named as \"{1}\".\nCreate this folder, move the file, and continue?",
+ "cantOpen": "A folder named \"{0}\" already exists. Can't open sketch.",
+ "saveFolderAs": "Save sketch folder as...",
+ "sketchbook": "Sketchbook",
+ "upload": "Upload",
+ "uploadUsingProgrammer": "Upload Using Programmer",
+ "doneUploading": "Done uploading.",
+ "couldNotConnectToMonitor": "Could not reconnect to serial monitor. {0}",
+ "verifyOrCompile": "Verify/Compile",
+ "exportBinary": "Export Compiled Binary",
+ "verify": "Verify",
+ "doneCompiling": "Done compiling.",
+ "openSketchInNewWindow": "Open Sketch in New Window",
+ "openFolder": "Open Folder",
+ "close": "Are you sure you want to close the sketch?"
+ },
+ "bootloader": {
+ "burnBootloader": "Burn Bootloader",
+ "doneBurningBootloader": "Done burning bootloader."
+ },
+ "debug": {
+ "debugWithMessage": "Debug - {0}",
+ "noPlatformInstalledFor": "Platform is not installed for '{0}'",
+ "debuggingNotSupported": "Debugging is not supported by '{0}'"
+ },
+ "editor": {
+ "copyForForum": "Copy for Forum (Markdown)",
+ "commentUncomment": "Comment/Uncomment",
+ "increaseIndent": "Increase Indent",
+ "decreaseIndent": "Decrease Indent",
+ "increaseFontSize": "Increase Font Size",
+ "decreaseFontSize": "Decrease Font Size",
+ "autoFormat": "Auto Format"
+ },
+ "examples": {
+ "menu": "Examples",
+ "couldNotInitializeExamples": "Could not initialize built-in examples.",
+ "builtInExamples": "Built-in examples",
+ "customLibrary": "Examples from Custom Libraries",
+ "for": "Examples for {0}",
+ "forAny": "Examples for any board"
+ },
+ "help": {
+ "search": "Search on Arduino.cc",
+ "keyword": "Type a keyword",
+ "gettingStarted": "Getting Started",
+ "environment": "Environment",
+ "troubleshooting": "Troubleshooting",
+ "reference": "Reference",
+ "findInReference": "Find in Reference",
+ "faq": "Frequently Asked Questions",
+ "visit": "Visit Arduino.cc"
+ },
+ "certificate": {
+ "uploadRootCertificates": "Upload SSL Root Certificates",
+ "openContext": "Open context",
+ "remove": "Remove",
+ "upload": "Upload"
+ },
+ "firmware": {
+ "updater": "WiFi101 / WiFiNINA Firmware Updater"
+ },
+ "dialog": {
+ "dontAskAgain": "Don't ask again"
+ },
+ "monitor": {
+ "connectionBusy": "Connection failed. Serial port is busy: {0}",
+ "disconnected": "Disconnected {0} from {1}.",
+ "unexpectedError": "Unexpected error. Reconnecting {0} on port {1}.",
+ "failedReconnect": "Failed to reconnect {0} to the the serial-monitor after 10 consecutive attempts. The {1} serial port is busy.",
+ "reconnect": "Reconnecting {0} to {1} in {2] seconds..."
+ },
+ "electron": {
+ "couldNotSave": "Could not save the sketch. Please copy your unsaved work into your favorite text editor, and restart the IDE.",
+ "unsavedChanges": "Any unsaved changes will not be saved."
+ }
+ },
+ "theia": {
+ "core": {
+ "couldNotSave": "Could not save the sketch. Please copy your unsaved work into your favorite text editor, and restart the IDE.",
+ "offline": "Offline",
+ "daemonOffline": "CLI Daemon Offline",
+ "cannotConnectBackend": "Cannot connect to the backend.",
+ "cannotConnectDaemon": "Cannot connect to the CLI daemon."
+ },
+ "debug": {
+ "start": "Start...",
+ "typeNotSupported": "The debug session type \"{0}\" is not supported.",
+ "startError": "There was an error starting the debug session, check the logs for more details."
+ },
+ "editor": {
+ "unsavedTitle": "Unsaved – {0}"
+ },
+ "workspace": {
+ "fileNewName": "Name for new file",
+ "invalidFilename": "Invalid filename.",
+ "invalidExtension": ".{0} is not a valid extension",
+ "newFileName": "New name for file",
+ "deleteCurrentSketch": "Do you want to delete the current sketch?",
+ "sketchDirectoryError": "There was an error creating the sketch directory. See the log for more details. The application will probably not work as expected."
+ }
+ }
+}
diff --git a/package.json b/package.json
index 278cee1e5..87a46a8b2 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
"node": ">=12.14.1 <13"
},
"devDependencies": {
- "@theia/cli": "1.17.2",
+ "@theia/cli": "1.19.0",
"@types/sinon": "^2.3.5",
"@types/jsdom": "^11.0.4",
"@typescript-eslint/eslint-plugin": "^4.27.0",
@@ -35,14 +35,18 @@
},
"scripts": {
"prepare": "cross-env THEIA_ELECTRON_SKIP_REPLACE_FFMPEG=1 lerna run prepare && yarn download:plugins",
- "cleanup": "npx rimraf ./**/node_modules && rm -rf ./node_modules ./.browser_modules ./arduino-ide-extension/build ./arduino-ide-extension/downloads ./arduino-ide-extension/Examples ./arduino-ide-extension/lib ./browser-app/lib ./browser-app/src-gen ./browser-app/gen-webpack.config.js ./electron-app/lib ./electron-app/src-gen ./electron-app/gen-webpack.config.js",
+ "cleanup": "npx rimraf ./**/node_modules && rm -rf ./node_modules ./.browser_modules ./arduino-ide-extension/build ./arduino-ide-extension/downloads ./arduino-ide-extension/Examples ./arduino-ide-extension/lib ./browser-app/lib ./browser-app/src-gen ./browser-app/gen-webpack.config.js ./electron-app/lib ./electron-app/src-gen ./electron-app/gen-webpack.config.js",
"rebuild:browser": "theia rebuild:browser",
"rebuild:electron": "theia rebuild:electron",
"start": "yarn --cwd ./electron-app start",
"watch": "lerna run watch --parallel",
"test": "lerna run test",
"download:plugins": "theia download:plugins",
- "update:version": "node ./scripts/update-version.js"
+ "update:version": "node ./scripts/update-version.js",
+ "i18n:generate": "theia nls-extract -e vscode -f '+(arduino-ide-extension|browser-app|electron|electron-app|plugins)/**/*.ts' -o ./i18n/en.json",
+ "i18n:check": "yarn i18n:generate && git add -N ./i18n && git diff --exit-code ./i18n",
+ "i18n:push": "node ./scripts/i18n/transifex-push.js ./i18n/en.json",
+ "i18n:pull": "node ./scripts/i18n/transifex-pull.js ./i18n/"
},
"lint-staged": {
"./arduino-ide-extension/**/*.{ts,tsx}": [
diff --git a/scripts/i18n/transifex-pull.js b/scripts/i18n/transifex-pull.js
new file mode 100644
index 000000000..986df91ea
--- /dev/null
+++ b/scripts/i18n/transifex-pull.js
@@ -0,0 +1,149 @@
+// @ts-check
+
+const transifex = require('./transifex');
+const util = require('util');
+const shell = require('shelljs');
+const fetch = require('node-fetch');
+const download = require('download');
+
+const getLanguages = async (organization, project) => {
+ const url = transifex.url(
+ util.format('projects/o:%s:p:%s/languages', organization, project)
+ );
+ const json = await fetch(url, { headers: transifex.authHeader() })
+ .catch(err => {
+ shell.echo(err);
+ shell.exit(1);
+ })
+ .then(res => res.json());
+ let languages = [];
+ json['data'].forEach(e => {
+ const languageCode = e['attributes']['code'];
+ // Skip english since it's the one we generate
+ if (languageCode === 'en') {
+ return;
+ }
+ languages.push(languageCode);
+ });
+ return languages;
+};
+
+const requestTranslationDownload = async (relationships) => {
+ let url = transifex.url('resource_translations_async_downloads');
+ const data = {
+ data: {
+ relationships,
+ type: 'resource_translations_async_downloads'
+ }
+ };
+ const headers = transifex.authHeader();
+ headers['Content-Type'] = 'application/vnd.api+json';
+ const json = await fetch(url, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify(data)
+ })
+ .catch(err => {
+ shell.echo(err);
+ shell.exit(1);
+ })
+ .then(res => res.json());
+
+ return json['data']['id'];
+};
+
+const getTranslationDownloadStatus = async (language, downloadRequestId) => {
+ // The download request status must be asked from time to time, if it's
+ // still pending we try again using exponentional backoff starting from 2.5 seconds.
+ let backoffMs = 2500;
+ while (true) {
+ const url = transifex.url(
+ util.format('resource_translations_async_downloads/%s', downloadRequestId)
+ );
+ const options = {
+ headers: transifex.authHeader(),
+ redirect: 'manual'
+ };
+ const res = await fetch(url, options).catch(err => {
+ shell.echo(err);
+ shell.exit(1);
+ });
+
+ if (res.status === 303) {
+ // When the file to download is ready we get redirected
+ return {
+ language,
+ downloadUrl: res.headers.get('location')
+ };
+ }
+
+ const json = await res.json();
+ const downloadStatus = json['data']['attributes']['status'];
+ if (downloadStatus == 'pending' || downloadStatus == 'processing') {
+ await new Promise(r => setTimeout(r, backoffMs));
+ backoffMs = backoffMs * 2;
+ // Retry the download request status again
+ continue;
+ } else if (downloadStatus == 'failed') {
+ const errors = [];
+ json['data']['attributes']['errors'].forEach(err => {
+ errors.push(util.format('%s: %s', err.code, err.details));
+ });
+ throw util.format('Download request failed: %s', errors.join(', '));
+ }
+ throw 'Download request failed in an unforeseen way';
+ }
+};
+
+(async () => {
+ const { organization, project, resource } = await transifex.credentials();
+ const translationsDirectory = process.argv[2];
+ if (!translationsDirectory) {
+ shell.echo('Traslations directory not specified');
+ shell.exit(1);
+ }
+
+ const languages = await getLanguages(organization, project);
+ shell.echo('translations found:', languages.join(', '));
+
+ let downloadIds = [];
+ for (const language of languages) {
+ downloadIds.push({
+ language,
+ id: await requestTranslationDownload({
+ language: {
+ data: {
+ id: util.format('l:%s', language),
+ type: 'languages'
+ }
+ },
+ resource: {
+ data: {
+ id: util.format('o:%s:p:%s:r:%s', organization, project, resource),
+ type: 'resources'
+ }
+ }
+ })
+ });
+ }
+
+ const res = await Promise.all(
+ downloadIds.map(d => getTranslationDownloadStatus(d['language'], d['id']))
+ ).catch(err => {
+ shell.echo(err);
+ shell.exit(1);
+ });
+
+ await Promise.all(
+ res.map(r => {
+ return download(r['downloadUrl'], translationsDirectory, {
+ filename: r['language'] + '.json'
+ });
+ })
+ ).catch(err => {
+ shell.echo(err);
+ shell.exit(1);
+ });
+
+ shell.echo('Translation files downloaded.');
+})();
diff --git a/scripts/i18n/transifex-push.js b/scripts/i18n/transifex-push.js
new file mode 100644
index 000000000..3eb9248f9
--- /dev/null
+++ b/scripts/i18n/transifex-push.js
@@ -0,0 +1,96 @@
+// @ts-check
+
+const transifex = require('./transifex');
+const fetch = require('node-fetch');
+const fs = require('fs');
+const shell = require('shelljs');
+const util = require('util');
+
+const uploadSourceFile = async (organization, project, resource, filePath) => {
+ const url = transifex.url('resource_strings_async_uploads');
+ const data = {
+ data: {
+ attributes: {
+ callback_url: null,
+ content: fs.readFileSync(filePath).toString('base64'),
+ content_encoding: 'base64'
+ },
+ relationships: {
+ resource: {
+ data: {
+ id: util.format('o:%s:p:%s:r:%s', organization, project, resource),
+ type: 'resources'
+ }
+ }
+ },
+ type: 'resource_strings_async_uploads'
+ }
+ };
+
+ const headers = transifex.authHeader();
+ headers['Content-Type'] = 'application/vnd.api+json';
+ const json = await fetch(url, { method: 'POST', headers, body: JSON.stringify(data) })
+ .catch(err => {
+ shell.echo(err);
+ shell.exit(1);
+ })
+ .then(res => res.json());
+
+ return json['data']['id'];
+};
+
+const getSourceUploadStatus = async (uploadId) => {
+ const url = transifex.url(util.format('resource_strings_async_uploads/%s', uploadId));
+ // The download request status must be asked from time to time, if it's
+ // still pending we try again using exponentional backoff starting from 2.5 seconds.
+ let backoffMs = 2500;
+ const headers = transifex.authHeader();
+ while (true) {
+ const json = await fetch(url, { headers })
+ .catch(err => {
+ shell.echo(err);
+ shell.exit(1);
+ })
+ .then(res => res.json());
+
+ const status = json['data']['attributes']['status'];
+ if (status === 'succeeded') {
+ return
+ } else if (status === 'pending' || status === 'processing') {
+ await new Promise(r => setTimeout(r, backoffMs));
+ backoffMs = backoffMs * 2;
+ // Retry the upload request status again
+ continue
+ } else if (status === 'failed') {
+ const errors = [];
+ json['data']['attributes']['errors'].forEach(err => {
+ errors.push(util.format('%s: %s', err.code, err.details));
+ });
+ throw util.format('Download request failed: %s', errors.join(', '));
+ }
+ throw 'Download request failed in an unforeseen way';
+ }
+}
+
+(async () => {
+ const { organization, project, resource } = await transifex.credentials();
+ const sourceFile = process.argv[2];
+ if (!sourceFile) {
+ shell.echo('Translation source file not specified');
+ shell.exit(1);
+ }
+
+ const uploadId = await uploadSourceFile(organization, project, resource, sourceFile)
+ .catch(err => {
+ shell.echo(err);
+ shell.exit(1);
+ });
+
+ await getSourceUploadStatus(uploadId)
+ .catch(err => {
+ shell.echo(err);
+ shell.exit(1);
+ });
+
+ shell.echo("Translation source file uploaded");
+})()
\ No newline at end of file
diff --git a/scripts/i18n/transifex.js b/scripts/i18n/transifex.js
new file mode 100644
index 000000000..b174feb26
--- /dev/null
+++ b/scripts/i18n/transifex.js
@@ -0,0 +1,52 @@
+// @ts-check
+
+const shell = require('shelljs');
+const util = require('util');
+
+const TRANSIFEX_ENDPOINT = 'https://rest.api.transifex.com/';
+
+const apiKey = () => {
+ const apiKey = process.env.TRANSIFEX_API_KEY;
+ if (apiKey === '') {
+ shell.echo('missing TRANSIFEX_API_KEY environment variable');
+ shell.exit(1)
+ }
+ return apiKey
+}
+
+exports.credentials = async () => {
+ const organization = process.env.TRANSIFEX_ORGANIZATION;
+ const project = process.env.TRANSIFEX_PROJECT;
+ const resource = process.env.TRANSIFEX_RESOURCE;
+
+ if (organization === '') {
+ shell.echo('missing TRANSIFEX_ORGANIZATION environment variable');
+ shell.exit(1)
+ }
+
+ if (project === '') {
+ shell.echo('missing TRANSIFEX_PROJECT environment variable');
+ shell.exit(1)
+ }
+
+ if (resource === '') {
+ shell.echo('missing TRANSIFEX_RESOURCE environment variable');
+ shell.exit(1)
+ }
+
+ return { organization, project, resource }
+}
+
+exports.url = (path, queryParameters) => {
+ let url = util.format('%s%s', TRANSIFEX_ENDPOINT, path);
+ if (queryParameters) {
+ url = util.format('%s?%s', url, queryParameters);
+ }
+ return url
+}
+
+exports.authHeader = () => {
+ return {
+ 'Authorization': util.format("Bearer %s", apiKey()),
+ }
+}
diff --git a/yarn.lock b/yarn.lock
index 9764f514f..ee92004da 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2136,6 +2136,38 @@
worker-loader "^3.0.8"
yargs "^15.3.1"
+"@theia/application-manager@1.19.0":
+ version "1.19.0"
+ resolved "https://registry.yarnpkg.com/@theia/application-manager/-/application-manager-1.19.0.tgz#e608e55e1bc5252b185e6f8917ea274de2a8dc7a"
+ integrity sha512-h4PspeMtDyHnpz3qELTKwUeWTFmpXe4OzDmET6u6NgXh93ba94gnIh0OZg7xjDD/3vrJLz1yQhVc263FioxBjw==
+ dependencies:
+ "@babel/core" "^7.10.0"
+ "@babel/plugin-transform-classes" "^7.10.0"
+ "@babel/plugin-transform-runtime" "^7.10.0"
+ "@babel/preset-env" "^7.10.0"
+ "@theia/application-package" "1.19.0"
+ "@theia/compression-webpack-plugin" "^3.0.0"
+ "@types/fs-extra" "^4.0.2"
+ babel-loader "^8.2.2"
+ buffer "^6.0.3"
+ circular-dependency-plugin "^5.2.2"
+ copy-webpack-plugin "^8.1.1"
+ css-loader "^6.2.0"
+ electron-rebuild "^1.8.6"
+ font-awesome-webpack "0.0.5-beta.2"
+ fs-extra "^4.0.2"
+ ignore-loader "^0.1.2"
+ less "^3.0.3"
+ setimmediate "^1.0.5"
+ source-map-loader "^2.0.1"
+ source-map-support "^0.5.19"
+ style-loader "^2.0.0"
+ umd-compat-loader "^2.1.2"
+ webpack "^5.48.0"
+ webpack-cli "4.7.0"
+ worker-loader "^3.0.8"
+ yargs "^15.3.1"
+
"@theia/application-package@1.17.2":
version "1.17.2"
resolved "https://registry.yarnpkg.com/@theia/application-package/-/application-package-1.17.2.tgz#05b1f2c749bbd693013f17cdf8b57d5789cb70fb"
@@ -2170,6 +2202,23 @@
semver "^5.4.1"
write-json-file "^2.2.0"
+"@theia/application-package@1.19.0":
+ version "1.19.0"
+ resolved "https://registry.yarnpkg.com/@theia/application-package/-/application-package-1.19.0.tgz#a6045bec3ebcc7acb80dbfbb644c7fd429799aeb"
+ integrity sha512-+WToWAofmvFafuY6kmsNAKz1IRwWwurHmj4U7JYpJ2jgey3H7lcO6qUMd8ZfWt3wK9ckPqlnuasuI7nHTZgcrA==
+ dependencies:
+ "@types/fs-extra" "^4.0.2"
+ "@types/request" "^2.0.3"
+ "@types/semver" "^5.4.0"
+ "@types/write-json-file" "^2.2.1"
+ changes-stream "^2.2.0"
+ deepmerge "^4.2.2"
+ fs-extra "^4.0.2"
+ is-electron "^2.1.0"
+ request "^2.82.0"
+ semver "^5.4.1"
+ write-json-file "^2.2.0"
+
"@theia/bulk-edit@1.18.0":
version "1.18.0"
resolved "https://registry.yarnpkg.com/@theia/bulk-edit/-/bulk-edit-1.18.0.tgz#bf24abd1fb25085e0c763beacfb13c2f084641dd"
@@ -2218,6 +2267,34 @@
temp "^0.9.1"
yargs "^15.3.1"
+"@theia/cli@1.19.0":
+ version "1.19.0"
+ resolved "https://registry.yarnpkg.com/@theia/cli/-/cli-1.19.0.tgz#d9316b5e294ad5e7c4f66fd6d44efd9419d60955"
+ integrity sha512-qMpI4cjVWzYbStN/+9wxyWCimKCVmrP+uSykRqlRFnRJcsLQC1om8ED98//9BgwYW5y3zSwB7rmBd/S0Io8tsQ==
+ dependencies:
+ "@theia/application-manager" "1.19.0"
+ "@theia/application-package" "1.19.0"
+ "@theia/localization-manager" "1.19.0"
+ "@theia/ovsx-client" "1.19.0"
+ "@types/chai" "^4.2.7"
+ "@types/mkdirp" "^0.5.2"
+ "@types/mocha" "^5.2.7"
+ "@types/node-fetch" "^2.5.7"
+ "@types/puppeteer" "^2.0.0"
+ "@types/requestretry" "^1.12.3"
+ "@types/tar" "^4.0.3"
+ chai "^4.2.0"
+ colors "^1.4.0"
+ decompress "^4.2.1"
+ https-proxy-agent "^5.0.0"
+ mocha "^7.0.0"
+ node-fetch "^2.6.0"
+ proxy-from-env "^1.1.0"
+ puppeteer "^2.0.0"
+ puppeteer-to-istanbul "^1.2.2"
+ temp "^0.9.1"
+ yargs "^15.3.1"
+
"@theia/compression-webpack-plugin@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@theia/compression-webpack-plugin/-/compression-webpack-plugin-3.0.0.tgz#3d1b932327caf33b218fd5d3d1a64a5dbee4324a"
@@ -2440,6 +2517,17 @@
ajv "^6.5.3"
jsonc-parser "^2.2.0"
+"@theia/localization-manager@1.19.0":
+ version "1.19.0"
+ resolved "https://registry.yarnpkg.com/@theia/localization-manager/-/localization-manager-1.19.0.tgz#6b6eab185c032b376f72c0ddc2a07e6a51ebd4ea"
+ integrity sha512-bzVZNraNU181CyRI7EVKbkFSM7Q/gZ79avXoZISTWNQSZBdU7mHJiSDDZIIl9qzJdjo5fxtml5oUI98TRw0lnA==
+ dependencies:
+ "@types/fs-extra" "^4.0.2"
+ deepmerge "^4.2.2"
+ fs-extra "^4.0.2"
+ glob "^7.2.0"
+ typescript "^4.4.3"
+
"@theia/markers@1.18.0":
version "1.18.0"
resolved "https://registry.yarnpkg.com/@theia/markers/-/markers-1.18.0.tgz#8e1b0c1c55727915f4ea9c384dcf0c7d250774b8"
@@ -2528,6 +2616,15 @@
bent "^7.1.0"
semver "^5.4.1"
+"@theia/ovsx-client@1.19.0":
+ version "1.19.0"
+ resolved "https://registry.yarnpkg.com/@theia/ovsx-client/-/ovsx-client-1.19.0.tgz#6d350c831c7e3280a10269b1ce72f4312896d441"
+ integrity sha512-TCdEURZTywMv7TbvFZpYjCF/mrB2ltu+9gVIk49eGAQISbmyWOoq3h9D4p+r+HTC+9TvBwAP6LSkvfHjuu+3tw==
+ dependencies:
+ "@types/bent" "^7.0.1"
+ bent "^7.1.0"
+ semver "^5.4.1"
+
"@theia/plugin-ext-vscode@1.18.0":
version "1.18.0"
resolved "https://registry.yarnpkg.com/@theia/plugin-ext-vscode/-/plugin-ext-vscode-1.18.0.tgz#ef1e44992c0fb5b52d02a4c9b93b1da94f3f9a94"
@@ -7571,7 +7668,7 @@ glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
once "^1.3.0"
path-is-absolute "^1.0.0"
-glob@^7.0.5:
+glob@^7.0.5, glob@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
@@ -13992,6 +14089,11 @@ typescript@^3.9.2:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.9.tgz#e69905c54bc0681d0518bd4d587cc6f2d0b1a674"
integrity sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==
+typescript@^4.4.3:
+ version "4.4.4"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c"
+ integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==
+
uc.micro@^1.0.1, uc.micro@^1.0.5:
version "1.0.6"
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"