From d4f11a99d44d9291297202598ea013bb9ae9de25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 15 Oct 2020 11:41:42 +0200 Subject: [PATCH] MOBILE-3565 gulp: Add language and jira gulp tasks --- gulp/dev-config.js | 69 ++++ gulp/git.js | 237 ++++++++++++ gulp/jira.js | 475 ++++++++++++++++++++++++ gulp/task-build-lang.js | 190 ++++++++++ gulp/task-push.js | 280 ++++++++++++++ gulp/url.js | 79 ++++ gulp/utils.js | 119 ++++++ gulpfile.js | 48 +++ package.json | 3 +- src/app/app.component.ts | 33 +- src/app/core/login/lang/en.json | 3 + src/app/core/login/login.module.ts | 3 +- src/app/core/login/pages/site/site.html | 2 +- src/app/services/lang.ts | 2 +- src/assets/countries/en.json | 251 +++++++++++++ src/assets/lang/en.json | 304 +++++++++++++++ src/assets/mimetypes/en.json | 54 +++ 17 files changed, 2146 insertions(+), 6 deletions(-) create mode 100644 gulp/dev-config.js create mode 100644 gulp/git.js create mode 100644 gulp/jira.js create mode 100644 gulp/task-build-lang.js create mode 100644 gulp/task-push.js create mode 100644 gulp/url.js create mode 100644 gulp/utils.js create mode 100644 gulpfile.js create mode 100644 src/app/core/login/lang/en.json create mode 100644 src/assets/countries/en.json create mode 100644 src/assets/lang/en.json create mode 100644 src/assets/mimetypes/en.json diff --git a/gulp/dev-config.js b/gulp/dev-config.js new file mode 100644 index 000000000..b270bf51f --- /dev/null +++ b/gulp/dev-config.js @@ -0,0 +1,69 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const fs = require('fs'); + +const DEV_CONFIG_FILE = '.moodleapp-dev-config'; + +/** + * Class to read and write dev-config data from a file. + */ +class DevConfig { + + constructor() { + this.loadFileData(); + } + + /** + * Get a setting. + * + * @param name Name of the setting to get. + * @param defaultValue Value to use if not found. + */ + get(name, defaultValue) { + return typeof this.config[name] != 'undefined' ? this.config[name] : defaultValue; + } + + /** + * Load file data to memory. + */ + loadFileData() { + if (!fs.existsSync(DEV_CONFIG_FILE)) { + this.config = {}; + + return; + } + + try { + this.config = JSON.parse(fs.readFileSync(DEV_CONFIG_FILE)); + } catch (error) { + console.error('Error reading dev config file.', error); + this.config = {}; + } + } + + /** + * Save some settings. + * + * @param settings Object with the settings to save. + */ + save(settings) { + this.config = Object.assign(this.config, settings); + + // Save the data in the dev file. + fs.writeFileSync(DEV_CONFIG_FILE, JSON.stringify(this.config, null, 4)); + } +} + +module.exports = new DevConfig(); diff --git a/gulp/git.js b/gulp/git.js new file mode 100644 index 000000000..f3f66298a --- /dev/null +++ b/gulp/git.js @@ -0,0 +1,237 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const exec = require('child_process').exec; +const fs = require('fs'); +const DevConfig = require('./dev-config'); +const Utils = require('./utils'); + +/** + * Class to run git commands. + */ +class Git { + + /** + * Create a patch. + * + * @param range Show only commits in the specified revision range. + * @param saveTo Path to the file to save the patch to. If not defined, the patch contents will be returned. + * @return Promise resolved when done. If saveTo not provided, it will return the patch contents. + */ + createPatch(range, saveTo) { + return new Promise((resolve, reject) => { + exec(`git format-patch ${range} --stdout`, (err, result) => { + if (err) { + reject(err || 'Cannot create patch.'); + return; + } + + if (!saveTo) { + resolve(result); + return; + } + + // Save it to a file. + const directory = saveTo.substring(0, saveTo.lastIndexOf('/')); + if (directory && directory != '.' && directory != '..' && !fs.existsSync(directory)) { + fs.mkdirSync(directory); + } + fs.writeFileSync(saveTo, result); + + resolve(); + }); + }); + } + + /** + * Get current branch. + * + * @return Promise resolved with the branch name. + */ + getCurrentBranch() { + return new Promise((resolve, reject) => { + exec('git branch --show-current', (err, branch) => { + if (branch) { + resolve(branch.replace('\n', '')); + } else { + reject (err || 'Current branch not found.'); + } + }); + }); + } + + /** + * Get the HEAD commit for a certain branch. + * + * @param branch Name of the branch. + * @param branchData Parsed branch data. If not provided it will be calculated. + * @return HEAD commit. + */ + async getHeadCommit(branch, branchData) { + if (!branchData) { + // Parse the branch to get the project and issue number. + branchData = Utils.parseBranch(branch); + } + + // Loop over the last commits to find the first commit messages that doesn't belong to the issue. + const commitsString = await this.log(50, branch, '%s_____%H'); + const commits = commitsString.split('\n'); + commits.pop(); // Remove last element, it's an empty string. + + for (let i = 0; i < commits.length; i++) { + const commit = commits[i]; + const match = Utils.getIssueFromCommitMessage(commit) == branchData.issue; + + if (i === 0 && !match) { + // Most recent commit doesn't belong to the issue. Stop looking. + break; + } + + if (!match) { + // The commit does not match any more, we found it! + return commit.split('_____')[1]; + } + } + + // Couldn't find the commit using the commit names, get the last commit in the integration branch. + const remote = DevConfig.get('upstreamRemote', 'origin'); + console.log(`Head commit not found using commit messages. Get last commit from ${remote}/integration`); + const hashes = await this.hashes(1, `${remote}/integration`); + + return hashes[0]; + } + + /** + * Get the URL of a certain remote. + * + * @param remote Remote name. + * @return Promise resolved with the remote URL. + */ + getRemoteUrl(remote) { + return new Promise((resolve, reject) => { + exec(`git remote get-url ${remote}`, (err, url) => { + if (url) { + resolve(url.replace('\n', '')); + } else { + reject (err || 'Remote not found.'); + } + }); + }); + } + + /** + * Return the latest hashes from git log. + * + * @param count Number of commits to display. + * @param range Show only commits in the specified revision range. + * @param format Pretty-print the contents of the commit logs in a given format. + * @return Promise resolved with the list of hashes. + */ + async hashes(count, range, format) { + format = format || '%H'; + + const hashList = await this.log(count, range, format); + + const hashes = hashList.split('\n'); + hashes.pop(); // Remove last element, it's an empty string. + + return hashes; + } + + /** + * Calls the log command and returns the raw output. + * + * @param count Number of commits to display. + * @param range Show only commits in the specified revision range. + * @param format Pretty-print the contents of the commit logs in a given format. + * @param path Show only commits that are enough to explain how the files that match the specified paths came to be. + * @return Promise resolved with the result. + */ + log(count, range, format, path) { + if (typeof count == 'undefined') { + count = 10; + } + + let command = 'git log'; + + if (count > 0) { + command += ` -n ${count} `; + } + if (format) { + command += ` --format=${format} `; + } + if (range){ + command += ` ${range} `; + } + if (path) { + command += ` -- ${path}`; + } + + return new Promise((resolve, reject) => { + exec(command, (err, result, stderr) => { + if (err) { + reject(err); + } else { + resolve(result); + } + }); + }); + } + + /** + * Return the latest titles of the commit messages. + * + * @param count Number of commits to display. + * @param range Show only commits in the specified revision range. + * @param path Show only commits that are enough to explain how the files that match the specified paths came to be. + * @return Promise resolved with the list of titles. + */ + async messages(count, range, path) { + count = typeof count != 'undefined' ? count : 10; + + const messageList = await this.log(count, range, '%s', path); + + const messages = messageList.split('\n'); + messages.pop(); // Remove last element, it's an empty string. + + return messages; + } + + /** + * Push a branch. + * + * @param remote Remote to use. + * @param branch Branch to push. + * @param force Whether to force the push. + * @return Promise resolved when done. + */ + push(remote, branch, force) { + return new Promise((resolve, reject) => { + let command = `git push ${remote} ${branch}`; + if (force) { + command += ' -f'; + } + + exec(command, (err, result, stderr) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + } +} + +module.exports = new Git(); diff --git a/gulp/jira.js b/gulp/jira.js new file mode 100644 index 000000000..90aeaa1c7 --- /dev/null +++ b/gulp/jira.js @@ -0,0 +1,475 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const exec = require('child_process').exec; +const https = require('https'); +const inquirer = require('inquirer'); +const fs = require('fs'); +const request = require('request'); // This lib is deprecated, but it was the only way I found to make upload files work. +const DevConfig = require('./dev-config'); +const Git = require('./git'); +const Url = require('./url'); +const Utils = require('./utils'); + +const apiVersion = 2; + +/** + * Class to interact with Jira. + */ +class Jira { + + /** + * Ask the password to the user. + * + * @return Promise resolved with the password. + */ + async askPassword() { + const data = await inquirer.prompt([ + { + type: 'password', + name: 'password', + message: `Please enter the password for the username ${this.username}.`, + }, + ]); + + return data.password; + } + + /** + * Ask the user the tracker data. + * + * @return Promise resolved with the data, rejected if cannot get. + */ + async askTrackerData() { + const data = await inquirer.prompt([ + { + type: 'input', + name: 'url', + message: 'Please enter the tracker URL.', + default: 'https://tracker.moodle.org/', + }, + { + type: 'input', + name: 'username', + message: 'Please enter your tracker username.', + }, + ]); + + DevConfig.save({ + 'tracker.url': data.url, + 'tracker.username': data.username, + }); + + return data; + } + + /** + * Build URL to perform requests to Jira. + * + * @param uri URI to add the the Jira URL. + * @return URL. + */ + buildRequestUrl(uri) { + return Utils.concatenatePaths([this.url, this.uri, '/rest/api/', apiVersion, uri]); + } + + /** + * Delete an attachment. + * + * @param attachmentId Attachment ID. + * @return Promise resolved when done. + */ + async deleteAttachment(attachmentId) { + const response = await this.request(`attachment/${attachmentId}`, 'DELETE'); + + if (response.status != 204) { + throw new Error('Could not delete the attachment'); + } + } + + /** + * Load the issue info from jira server using a REST API call. + * + * @param key Key to identify the issue. E.g. MOBILE-1234. + * @param fields Fields to get. + * @return Promise resolved with the issue data. + */ + async getIssue(key, fields) { + fields = fields || '*all,-comment'; + + await this.init(); // Initialize data if needed. + + const response = await this.request(`issue/${key}`, 'GET', {'fields': fields, 'expand': 'names'}); + + if (response.status == 404) { + throw new Error('Issue could not be found.'); + } else if (response.status != 200) { + throw new Error('The tracker is not available.') + } + + const issue = response.data; + issue.named = {}; + + // Populate the named fields in a separate key. Allows us to easily find them without knowing the field ID. + const nameList = issue.names || {}; + for (const fieldKey in issue.fields) { + if (nameList[fieldKey]) { + issue.named[nameList[fieldKey]] = issue.fields[fieldKey]; + } + } + + return issue + } + + /** + * Load the version info from the jira server using a rest api call. + * + * @return Promise resolved when done. + */ + async getServerInfo() { + const response = await this.request('serverInfo'); + + if (response.status != 200) { + throw new Error(`Unexpected response code: ${response.status}`, response); + } + + this.version = response.data; + } + + /** + * Get tracker data to push an issue. + * + * @return Promise resolved with the data. + */ + async getTrackerData() { + // Check dev-config file first. + let data = this.getTrackerDataFromDevConfig(); + + if (data) { + console.log('Using tracker data from dev-config file'); + return data; + } + + // Try to use mdk now. + try { + data = await this.getTrackerDataFromMdk(); + + console.log('Using tracker data from mdk'); + + return data; + } catch (error) { + // MDK not available or not configured. Ask for the data. + const data = await this.askTrackerData(); + + data.fromInput = true; + + return data; + } + } + + /** + * Get tracker data from dev config file. + * + * @return Data, undefined if cannot get. + */ + getTrackerDataFromDevConfig() { + const url = DevConfig.get('tracker.url'); + const username = DevConfig.get('tracker.username'); + + if (url && username) { + return { + url, + username, + }; + } + } + + /** + * Get tracker URL and username from mdk. + * + * @return Promise resolved with the data, rejected if cannot get. + */ + getTrackerDataFromMdk() { + return new Promise((resolve, reject) => { + exec('mdk config show tracker.url', (err, url) => { + if (!url) { + reject(err || 'URL not found.'); + return; + } + + exec('mdk config show tracker.username', (err, username) => { + if (username) { + resolve({ + url: url.replace('\n', ''), + username: username.replace('\n', ''), + }); + } else { + reject(err | 'Username not found.'); + } + }); + }); + }); + } + + /** + * Initialize some data. + * + * @return Promise resolved when done. + */ + async init() { + if (this.initialized) { + // Already initialized. + return; + } + + // Get tracker URL and username. + const trackerData = await this.getTrackerData(); + + this.url = trackerData.url; + this.username = trackerData.username; + + const parsed = Url.parse(this.url); + this.ssl = parsed.protocol == 'https'; + this.host = parsed.domain; + this.uri = parsed.path; + + // Get the password. + const keytar = require('keytar'); + + this.password = await keytar.getPassword('mdk-jira-password', this.username); // Use same service name as mdk. + + if (!this.password) { + // Ask the user. + this.password = await this.askPassword(); + } + + while (!this.initialized) { + try { + await this.getServerInfo(); + + this.initialized = true; + keytar.setPassword('mdk-jira-password', this.username, this.password); + } catch (error) { + console.log('Error connecting to the server. Please make sure you entered the data correctly.', error); + if (trackerData.fromInput) { + // User entered the data manually, ask him again. + trackerData = await this.askTrackerData(); + + this.url = trackerData.url; + this.username = trackerData.username; + } + + this.password = await this.askPassword(); + } + } + } + + /** + * Check if a certain issue could be a security issue. + * + * @param key Key to identify the issue. E.g. MOBILE-1234. + * @return Promise resolved with boolean: whether it's a security issue. + */ + async isSecurityIssue(key) { + const issue = await this.getIssue(key, 'security'); + + return issue.fields && !!issue.fields.security; + } + + /** + * Sends a request to the server and returns the data. + * + * @param uri URI to add the the Jira URL. + * @param method Method to use. Defaults to 'GET'. + * @param params Params to send as GET params (in the URL). + * @param data JSON string with the data to send as POST/PUT params. + * @param headers Headers to send. + * @return Promise resolved with the result. + */ + request(uri, method, params, data, headers) { + uri = uri || ''; + method = (method || 'GET').toUpperCase(); + data = data || ''; + params = params || {}; + headers = headers || {}; + headers['Content-Type'] = 'application/json'; + + return new Promise((resolve, reject) => { + + // Build the request URL. + const url = Url.addParamsToUrl(this.buildRequestUrl(uri), params); + + // Initialize the request. + const options = { + method: method, + auth: `${this.username}:${this.password}`, + headers: headers, + }; + const request = https.request(url, options); + + // Add data. + if (data) { + request.write(data); + } + + // Treat response. + request.on('response', (response) => { + // Read the result. + let result = ''; + response.on('data', (chunk) => { + result += chunk; + }); + response.on('end', () => { + try { + result = JSON.parse(result); + } catch (error) { + // Leave it as text. + } + + resolve({ + status: response.statusCode, + data: result, + }); + }); + }); + + request.on('error', (e) => { + reject(e); + }); + + // Send the request. + request.end(); + }); + } + + /** + * Sets a set of fields for a certain issue in Jira. + * + * @param key Key to identify the issue. E.g. MOBILE-1234. + * @param updates Object with the fields to update. + * @return Promise resolved when done. + */ + async setCustomFields(key, updates) { + const issue = await this.getIssue(key); + const update = {'fields': {}}; + + // Detect which fields have changed. + for (const updateName in updates) { + const updateValue = updates[updateName]; + const remoteValue = issue.named[updateName]; + + if (!remoteValue || remoteValue != updateValue) { + // Map the label of the field with the field code. + let fieldKey; + for (const key in issue.names) { + if (issue.names[key] == updateName) { + fieldKey = key; + break; + } + } + + if (!fieldKey) { + throw new Error(`Could not find the field named ${updateName}.`); + } + + update.fields[fieldKey] = updateValue; + } + } + + if (!Object.keys(update.fields).length) { + // No fields to update. + console.log('No updates required.') + return; + } + + const response = await this.request(`issue/${key}`, 'PUT', null, JSON.stringify(update)); + + if (response.status != 204) { + throw new Error(`Issue was not updated: ${response.status}`, response.data); + } + + console.log('Issue updated successfully.'); + } + + /** + * Upload a new attachment to an issue. + * + * @param key Key to identify the issue. E.g. MOBILE-1234. + * @param filePath Path to the file to upload. + * @return Promise resolved when done. + */ + async upload(key, filePath) { + + const uri = `issue/${key}/attachments`; + const headers = { + 'X-Atlassian-Token': 'nocheck', + } + + const response = await this.uploadFile(uri, 'file', filePath, headers); + + if (response.status != 200) { + throw new Error('Could not upload file to Jira issue'); + } + + console.log('File successfully uploaded.') + } + + /** + * Upload a file to Jira. + * + * @param uri URI to add the the Jira URL. + * @param fieldName Name of the form field where to put the file. + * @param filePath Path to the file. + * @param headers Headers. + * @return Promise resolved with the result. + */ + async uploadFile(uri, fieldName, filePath, headers) { + uri = uri || ''; + headers = headers || {}; + headers['Content-Type'] = 'multipart/form-data'; + + return new Promise((resolve, reject) => { + // Add the file to the form data. + const formData = {}; + formData[fieldName] = { + value: fs.createReadStream(filePath), + options: { + filename: filePath.substr(filePath.lastIndexOf('/') + 1), + contentType: 'multipart/form-data', + }, + }; + + // Perform the request. + const options = { + url: this.buildRequestUrl(uri), + method: 'POST', + headers: headers, + auth: { + user: this.username, + pass: this.password, + }, + formData: formData, + }; + + request(options, (err, httpResponse, body) => { + resolve({ + status: httpResponse.statusCode, + data: body, + }); + }); + }); + } +} + +module.exports = new Jira(); diff --git a/gulp/task-build-lang.js b/gulp/task-build-lang.js new file mode 100644 index 000000000..980bf52a4 --- /dev/null +++ b/gulp/task-build-lang.js @@ -0,0 +1,190 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const gulp = require('gulp'); +const slash = require('gulp-slash'); +const clipEmptyFiles = require('gulp-clip-empty-files'); +const through = require('through'); +const bufferFrom = require('buffer-from'); +const File = require('vinyl'); +const pathLib = require('path'); + +/** + * Task to build the language files into a single file per language. + */ +class BuildLangTask { + + /** + * Copy a property from one object to another, adding a prefix to the key if needed. + * + * @param target Object to copy the properties to. + * @param source Object to copy the properties from. + * @param prefix Prefix to add to the keys. + */ + addProperties(target, source, prefix) { + for (let property in source) { + target[prefix + property] = source[property]; + } + } + + /** + * Run the task. + * + * @param language Language to treat. + * @param langPaths Paths to the possible language files. + * @param done Function to call when done. + */ + run(language, langPaths, done) { + const filename = language + '.json'; + const data = {}; + let firstFile = null; + const self = this; + + const paths = langPaths.map((path) => { + if (path.slice(-1) != '/') { + path = path + '/'; + } + + return path + language + '.json'; + }); + + gulp.src(paths, { allowEmpty: true }) + .pipe(slash()) + .pipe(clipEmptyFiles()) + .pipe(through(function(file) { + if (!firstFile) { + firstFile = file; + } + + return self.treatFile(file, data); + }, function() { + /* This implementation is based on gulp-jsoncombine module. + * https://github.com/reflog/gulp-jsoncombine */ + if (firstFile) { + const joinedPath = pathLib.join(firstFile.base, language + '.json'); + + const joinedFile = new File({ + cwd: firstFile.cwd, + base: firstFile.base, + path: joinedPath, + contents: self.treatMergedData(data), + }); + + this.emit('data', joinedFile); + } + + this.emit('end'); + })) + .pipe(gulp.dest(pathLib.join('./src/assets', 'lang'))) + .on('end', done); + } + + /** + * Treats a file to merge JSONs. This function is based on gulp-jsoncombine module. + * https://github.com/reflog/gulp-jsoncombine + * + * @param file File treated. + * @param data Object where to store the data. + */ + treatFile(file, data) { + if (file.isNull() || file.isStream()) { + return; // ignore + } + + try { + let path = file.path; + let length = 9; + + let srcPos = path.lastIndexOf('/src/app/'); + if (srcPos < 0) { + // It's probably a Windows environment. + srcPos = path.lastIndexOf('\\src\\app\\'); + } + if (srcPos < 0) { + length = 5; + srcPos = path.lastIndexOf('/src/'); + if (srcPos < 0) { + // It's probably a Windows environment. + srcPos = path.lastIndexOf('\\src\\'); + } + } + path = path.substr(srcPos + length); + + data[path] = JSON.parse(file.contents.toString()); + } catch (err) { + console.log('Error parsing JSON: ' + err); + } + } + + /** + * Treats the merged JSON data, adding prefixes depending on the component. + * + * @param data Merged data. + * @return Buffer with the treated data. + */ + treatMergedData(data) { + const merged = {}; + const mergedOrdered = {}; + + for (let filepath in data) { + + const pathSplit = filepath.split(/[\/\\]/); + let prefix; + + pathSplit.pop(); + + switch (pathSplit[0]) { + case 'lang': + prefix = 'core'; + break; + case 'core': + if (pathSplit[1] == 'lang') { + // Not used right now. + prefix = 'core'; + } else { + prefix = 'core.' + pathSplit[1]; + } + break; + case 'addon': + // Remove final item 'lang'. + pathSplit.pop(); + // Remove first item 'addon'. + pathSplit.shift(); + + // For subplugins. We'll use plugin_subfolder_subfolder2_... + // E.g. 'mod_assign_feedback_comments'. + prefix = 'addon.' + pathSplit.join('_'); + break; + case 'assets': + prefix = 'assets.' + pathSplit[1]; + break; + } + + if (prefix) { + this.addProperties(merged, data[filepath], prefix + '.'); + } + } + + + + // Force ordering by string key. + Object.keys(merged).sort().forEach((key) => { + mergedOrdered[key] = merged[key]; + }); + + return bufferFrom(JSON.stringify(mergedOrdered, null, 4)); + } +} + +module.exports = BuildLangTask; diff --git a/gulp/task-push.js b/gulp/task-push.js new file mode 100644 index 000000000..54d151dde --- /dev/null +++ b/gulp/task-push.js @@ -0,0 +1,280 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const gulp = require('gulp'); +const inquirer = require('inquirer'); +const DevConfig = require('./dev-config'); +const Git = require('./git'); +const Jira = require('./jira'); +const Utils = require('./utils'); + +/** + * Task to push a git branch and update tracker issue. + */ +class PushTask { + + /** + * Ask the user whether he wants to continue. + * + * @return Promise resolved with boolean: true if he wants to continue. + */ + async askConfirmContinue() { + const answer = await inquirer.prompt([ + { + type: 'input', + name: 'confirm', + message: 'Are you sure you want to continue?', + default: 'n', + }, + ]); + + return answer.confirm == 'y'; + } + + /** + * Push a patch to the tracker and remove the previous one. + * + * @param branch Branch name. + * @param branchData Parsed branch data. + * @param remote Remote used. + * @return Promise resolved when done. + */ + async pushPatch(branch, branchData, remote) { + const headCommit = await Git.getHeadCommit(branch, branchData); + + if (!headCommit) { + throw new Error('Head commit not resolved, abort pushing patch.'); + } + + // Create the patch file. + const fileName = branch + '.patch'; + const tmpPatchPath = `./tmp/${fileName}`; + + await Git.createPatch(`${headCommit}...${branch}`, tmpPatchPath); + console.log('Git patch created'); + + // Check if there is an attachment with same name in the issue. + const issue = await Jira.getIssue(branchData.issue, 'attachment'); + + let existingAttachmentId; + const attachments = (issue.fields && issue.fields.attachment) || []; + for (const i in attachments) { + if (attachments[i].filename == fileName) { + // Found an existing attachment with the same name, we keep track of it. + existingAttachmentId = attachments[i].id; + break + } + } + + // Push the patch to the tracker. + console.log(`Uploading patch ${fileName} to the tracker...`); + await Jira.upload(branchData.issue, tmpPatchPath); + + if (existingAttachmentId) { + // On success, deleting file that was there before. + try { + console.log('Deleting older patch...') + await Jira.deleteAttachment(existingAttachmentId); + } catch (error) { + console.log('Could not delete older attachment.'); + } + } + } + + /** + * Run the task. + * + * @param args Command line arguments. + * @param done Function to call when done. + */ + async run(args, done) { + try { + const remote = args.remote || DevConfig.get('upstreamRemote', 'origin'); + let branch = args.branch; + const force = !!args.force; + + if (!branch) { + branch = await Git.getCurrentBranch(); + } + + if (!branch) { + throw new Error('Cannot determine the current branch. Please make sure youu aren\'t in detached HEAD state'); + } else if (branch == 'HEAD') { + throw new Error('Cannot push HEAD branch'); + } + + // Parse the branch to get the project and issue number. + const branchData = Utils.parseBranch(branch); + const keepRunning = await this.validateCommitMessages(branchData); + + if (!keepRunning) { + // Last commit not valid, stop. + console.log('Exiting...'); + done(); + return; + } + + if (!args.patch) { + // Check if it's a security issue to force patch mode. + try { + args.patch = await Jira.isSecurityIssue(branchData.issue); + + if (args.patch) { + console.log(`${branchData.issue} appears to be a security issue, switching to patch mode...`); + } + } catch (error) { + console.log(`Could not check if ${branchData.issue} is a security issue.`); + } + } + + if (args.patch) { + // Create and upload a patch file. + await this.pushPatch(branch, branchData, remote); + } else { + // Push the branch. + console.log(`Pushing branch ${branch} to remote ${remote}...`); + await Git.push(remote, branch, force); + + // Update tracker info. + console.log(`Branch pushed, update tracker info...`); + await this.updateTrackerGitInfo(branch, branchData, remote); + } + } catch (error) { + console.error(error); + } + + done(); + } + + /** + * Update git info in the tracker issue. + * + * @param branch Branch name. + * @param branchData Parsed branch data. + * @param remote Remote used. + * @return Promise resolved when done. + */ + async updateTrackerGitInfo(branch, branchData, remote) { + // Get the repository data for the project. + let repositoryUrl = DevConfig.get(branchData.project + '.repositoryUrl'); + let diffUrlTemplate = DevConfig.get(branchData.project + '.diffUrlTemplate', ''); + + if (!repositoryUrl) { + // Calculate the repositoryUrl based on the remote URL. + repositoryUrl = await Git.getRemoteUrl(remote); + } + + // Make sure the repository URL uses the regular format. + repositoryUrl = repositoryUrl.replace(/^(git@|git:\/\/)/, 'https://') + .replace(/\.git$/, '') + .replace('github.com:', 'github.com/'); + + if (!diffUrlTemplate) { + diffUrlTemplate = Utils.concatenatePaths([repositoryUrl, 'compare/%headcommit%...%branch%']); + } + + // Now create the git URL for the repository. + const repositoryGitUrl = repositoryUrl.replace(/^https?:\/\//, 'git://') + '.git'; + + // Search HEAD commit to put in the diff URL. + console.log ('Searching for head commit...'); + let headCommit = await Git.getHeadCommit(branch, branchData); + + if (!headCommit) { + throw new Error('Head commit not resolved, aborting update of tracker fields'); + } + + headCommit = headCommit.substr(0, 10); + console.log(`Head commit resolved to ${headCommit}`); + + // Calculate last properties needed. + const diffUrl = diffUrlTemplate.replace('%branch%', branch).replace('%headcommit%', headCommit); + const fieldRepositoryUrl = DevConfig.get('tracker.fieldnames.repositoryurl', 'Pull from Repository'); + const fieldBranch = DevConfig.get('tracker.fieldnames.branch', 'Pull Master Branch'); + const fieldDiffUrl = DevConfig.get('tracker.fieldnames.diffurl', 'Pull Master Diff URL'); + + // Update tracker fields. + const updates = {}; + updates[fieldRepositoryUrl] = repositoryGitUrl; + updates[fieldBranch] = branch; + updates[fieldDiffUrl] = diffUrl; + + console.log('Setting tracker fields...'); + await Jira.setCustomFields(branchData.issue, updates); + } + + /** + * Validate commit messages comparing them with the branch name. + * + * @param branchData Parsed branch data. + * @return True if value is ok or the user wants to continue anyway, false to stop. + */ + async validateCommitMessages(branchData) { + const messages = await Git.messages(30); + + let numConsecutive = 0; + let wrongCommitCandidate = null; + + for (let i = 0; i < messages.length; i++) { + const message = messages[i]; + const issue = Utils.getIssueFromCommitMessage(message); + + if (!issue || issue != branchData.issue) { + if (i === 0) { + // Last commit is wrong, it shouldn't happen. Ask the user if he wants to continue. + if (!issue) { + console.log('The issue number could not be found in the last commit message.'); + console.log(`Commit: ${message}`); + } else if (issue != branchData.issue) { + console.log('The issue number in the last commit does not match the branch being pushed to.'); + console.log(`Branch: ${branchData.issue} vs. commit: ${issue}`); + } + + return this.askConfirmContinue(); + } + + numConsecutive++; + if (numConsecutive > 2) { + // 3 consecutive commits with different branch, probably the branch commits are over. Everything OK. + return true; + + // Don't treat a merge pull request commit as a wrong commit between right commits. + // The current push could be a quick fix after a merge. + } else if (!wrongCommitCandidate && message.indexOf('Merge pull request') == -1) { + wrongCommitCandidate = { + message: message, + issue: issue, + index: i, + }; + } + } else if (wrongCommitCandidate) { + // We've found a commit with the branch name after a commit with a different branch. Probably wrong commit. + if (!wrongCommitCandidate.issue) { + console.log('The issue number could not be found in one of the commit messages.'); + console.log(`Commit: ${wrongCommitCandidate.message}`); + } else { + console.log('The issue number in a certain commit does not match the branch being pushed to.'); + console.log(`Branch: ${branchData.issue} vs. commit: ${wrongCommitCandidate.issue}`); + console.log(`Commit message: ${wrongCommitCandidate.message}`); + } + + return this.askConfirmContinue(); + } + } + + return true; + } +} + +module.exports = PushTask; diff --git a/gulp/url.js b/gulp/url.js new file mode 100644 index 000000000..8b46409f6 --- /dev/null +++ b/gulp/url.js @@ -0,0 +1,79 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * Class with helper functions for urls. + */ +class Url { + + /** + * Add params to a URL. + * + * @param url URL to add the params to. + * @param params Object with the params to add. + * @return URL with params. + */ + static addParamsToUrl(url, params) { + let separator = url.indexOf('?') != -1 ? '&' : '?'; + + for (const key in params) { + let value = params[key]; + + // Ignore objects. + if (typeof value != 'object') { + url += separator + key + '=' + value; + separator = '&'; + } + } + + return url; + } + + /** + * Parse parts of a url, using an implicit protocol if it is missing from the url. + * + * @param url Url. + * @return Url parts. + */ + static parse(url) { + // Parse url with regular expression taken from RFC 3986: https://tools.ietf.org/html/rfc3986#appendix-B. + const match = url.trim().match(/^(([^:/?#]+):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/); + + if (!match) { + return null; + } + + const host = match[4] || ''; + + // Get the credentials and the port from the host. + const [domainAndPort, credentials] = host.split('@').reverse(); + const [domain, port] = domainAndPort.split(':'); + const [username, password] = credentials ? credentials.split(':') : []; + + // Prepare parts replacing empty strings with undefined. + return { + protocol: match[2] || undefined, + domain: domain || undefined, + port: port || undefined, + credentials: credentials || undefined, + username: username || undefined, + password: password || undefined, + path: match[5] || undefined, + query: match[7] || undefined, + fragment: match[9] || undefined, + }; + } +} + +module.exports = Url; diff --git a/gulp/utils.js b/gulp/utils.js new file mode 100644 index 000000000..0ca11fa74 --- /dev/null +++ b/gulp/utils.js @@ -0,0 +1,119 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const DevConfig = require('./dev-config'); +const DEFAULT_ISSUE_REGEX = '^(MOBILE)[-_]([0-9]+)'; + +/** + * Class with some utility functions. + */ +class Utils { + /** + * Concatenate several paths, adding a slash between them if needed. + * + * @param paths List of paths. + * @return Concatenated path. + */ + static concatenatePaths(paths) { + if (!paths.length) { + return ''; + } + + // Remove all slashes between paths. + for (let i = 0; i < paths.length; i++) { + if (!paths[i]) { + continue; + } + + if (i === 0) { + paths[i] = String(paths[i]).replace(/\/+$/g, ''); + } else if (i === paths.length - 1) { + paths[i] = String(paths[i]).replace(/^\/+/g, ''); + } else { + paths[i] = String(paths[i]).replace(/^\/+|\/+$/g, ''); + } + } + + // Remove empty paths. + paths = paths.filter(path => !!path); + + return paths.join('/'); + } + + /** + * Get command line arguments. + * + * @return Object with command line arguments. + */ + static getCommandLineArguments() { + + let args = {}, opt, thisOpt, curOpt; + for (let a = 0; a < process.argv.length; a++) { + + thisOpt = process.argv[a].trim(); + opt = thisOpt.replace(/^\-+/, ''); + + if (opt === thisOpt) { + // argument value + if (curOpt) { + args[curOpt] = opt; + } + curOpt = null; + } + else { + // Argument name. + curOpt = opt; + args[curOpt] = true; + } + } + + return args; + } + + /** + * Given a commit message, return the issue name (e.g. MOBILE-1234). + * + * @param commit Commit message. + * @return Issue name. + */ + static getIssueFromCommitMessage(commit) { + const regex = new RegExp(DevConfig.get('wording.branchRegex', DEFAULT_ISSUE_REGEX), 'i'); + const matches = commit.match(regex); + + return matches && matches[0]; + } + + /** + * Parse a branch name to extract some data. + * + * @param branch Branch name to parse. + * @return Data. + */ + static parseBranch(branch) { + const regex = new RegExp(DevConfig.get('wording.branchRegex', DEFAULT_ISSUE_REGEX), 'i'); + + const matches = branch.match(regex); + if (!matches || matches.length < 3) { + throw new Error(`Error parsing branch ${branch}`); + } + + return { + issue: matches[0], + project: matches[1], + issueNumber: matches[2], + }; + } +} + +module.exports = Utils; diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 000000000..7a08d6c88 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,48 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const BuildLangTask = require('./gulp/task-build-lang'); +const PushTask = require('./gulp/task-push'); +const Utils = require('./gulp/utils'); +const gulp = require('gulp'); + +const paths = { + lang: [ + './src/app/lang/', + './src/app/core/**/lang/', + './src/app/addon/**/lang/', + './src/app/**/**/lang/', + './src/assets/countries/', + './src/assets/mimetypes/' + ] +}; + +const args = Utils.getCommandLineArguments(); + +// Build the language files into a single file per language. +gulp.task('lang', (done) => { + new BuildLangTask().run('en', paths.lang, done); +}); + +gulp.task('push', (done) => { + new PushTask().run(args, done); +}); + +gulp.task('default', gulp.parallel('lang')); + +gulp.task('watch', () => { + const langsPaths = paths.lang.map(path => path + 'en.json'); + + gulp.watch(langsPaths, { interval: 500 }, gulp.parallel('lang')); +}); diff --git a/package.json b/package.json index 80b676724..72bd72e26 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "test:ci": "jest -ci --runInBand --verbose", "test:watch": "jest --watch", "test:coverage": "jest --coverage", - "lint": "ng lint" + "lint": "ng lint", + "ionic:serve:before": "npx gulp" }, "dependencies": { "@angular/common": "~10.0.0", diff --git a/src/app/app.component.ts b/src/app/app.component.ts index dde487788..48afedd0e 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -12,11 +12,40 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; +import { CoreLangProvider } from '@services/lang'; +import { CoreEvents, CoreEventsProvider } from '@services/events'; @Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrls: ['app.component.scss'], }) -export class AppComponent { } +export class AppComponent implements OnInit { + + constructor( + private langProvider: CoreLangProvider, + ) { + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + CoreEvents.instance.on(CoreEventsProvider.LOGOUT, () => { + // Go to sites page when user is logged out. + // Due to DeepLinker, we need to use the ViewCtrl instead of name. + // Otherwise some pages are re-created when they shouldn't. + // TODO + // CoreApp.instance.getRootNavController().setRoot(CoreLoginSitesPage); + + // Unload lang custom strings. + this.langProvider.clearCustomStrings(); + + // Remove version classes from body. + // TODO + // this.removeVersionClass(); + }); + } + +} diff --git a/src/app/core/login/lang/en.json b/src/app/core/login/lang/en.json new file mode 100644 index 000000000..e776074e5 --- /dev/null +++ b/src/app/core/login/lang/en.json @@ -0,0 +1,3 @@ +{ + "yourenteredsite": "Connect to your site" +} diff --git a/src/app/core/login/login.module.ts b/src/app/core/login/login.module.ts index 74707b4fb..39ba3ce72 100644 --- a/src/app/core/login/login.module.ts +++ b/src/app/core/login/login.module.ts @@ -16,17 +16,18 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; import { CoreLoginRoutingModule } from './login-routing.module'; import { CoreLoginInitPage } from './pages/init/init.page'; import { CoreLoginSitePage } from './pages/site/site.page'; - @NgModule({ imports: [ CommonModule, IonicModule, CoreLoginRoutingModule, + TranslateModule.forChild(), ], declarations: [ CoreLoginInitPage, diff --git a/src/app/core/login/pages/site/site.html b/src/app/core/login/pages/site/site.html index 49d7a9823..e2beae84f 100644 --- a/src/app/core/login/pages/site/site.html +++ b/src/app/core/login/pages/site/site.html @@ -1,3 +1,3 @@ - Site page. + {{ 'core.login.yourenteredsite' | translate }} diff --git a/src/app/services/lang.ts b/src/app/services/lang.ts index 5a763614d..dc405bd31 100644 --- a/src/app/services/lang.ts +++ b/src/app/services/lang.ts @@ -246,7 +246,7 @@ export class CoreLangProvider { protected async detectLanguage(): Promise { // Get current language from config (user might have changed it). try { - return CoreConfig.instance.get('current_language'); + return await CoreConfig.instance.get('current_language'); } catch (e) { // Try will return, ignore errors here to avoid nesting. } diff --git a/src/assets/countries/en.json b/src/assets/countries/en.json new file mode 100644 index 000000000..777763e84 --- /dev/null +++ b/src/assets/countries/en.json @@ -0,0 +1,251 @@ +{ + "AD": "Andorra", + "AE": "United Arab Emirates", + "AF": "Afghanistan", + "AG": "Antigua and Barbuda", + "AI": "Anguilla", + "AL": "Albania", + "AM": "Armenia", + "AO": "Angola", + "AQ": "Antarctica", + "AR": "Argentina", + "AS": "American Samoa", + "AT": "Austria", + "AU": "Australia", + "AW": "Aruba", + "AX": "Åland Islands", + "AZ": "Azerbaijan", + "BA": "Bosnia and Herzegovina", + "BB": "Barbados", + "BD": "Bangladesh", + "BE": "Belgium", + "BF": "Burkina Faso", + "BG": "Bulgaria", + "BH": "Bahrain", + "BI": "Burundi", + "BJ": "Benin", + "BL": "Saint Barthélemy", + "BM": "Bermuda", + "BN": "Brunei Darussalam", + "BO": "Bolivia (Plurinational State of)", + "BQ": "Bonaire, Sint Eustatius and Saba", + "BR": "Brazil", + "BS": "Bahamas", + "BT": "Bhutan", + "BV": "Bouvet Island", + "BW": "Botswana", + "BY": "Belarus", + "BZ": "Belize", + "CA": "Canada", + "CC": "Cocos (Keeling) Islands", + "CD": "Congo (the Democratic Republic of the)", + "CF": "Central African Republic", + "CG": "Congo", + "CH": "Switzerland", + "CI": "Côte d'Ivoire", + "CK": "Cook Islands", + "CL": "Chile", + "CM": "Cameroon", + "CN": "China", + "CO": "Colombia", + "CR": "Costa Rica", + "CU": "Cuba", + "CV": "Cabo Verde", + "CW": "Curaçao", + "CX": "Christmas Island", + "CY": "Cyprus", + "CZ": "Czechia", + "DE": "Germany", + "DJ": "Djibouti", + "DK": "Denmark", + "DM": "Dominica", + "DO": "Dominican Republic", + "DZ": "Algeria", + "EC": "Ecuador", + "EE": "Estonia", + "EG": "Egypt", + "EH": "Western Sahara", + "ER": "Eritrea", + "ES": "Spain", + "ET": "Ethiopia", + "FI": "Finland", + "FJ": "Fiji", + "FK": "Falkland Islands (Malvinas)", + "FM": "Micronesia (Federated States of)", + "FO": "Faroe Islands", + "FR": "France", + "GA": "Gabon", + "GB": "United Kingdom", + "GD": "Grenada", + "GE": "Georgia", + "GF": "French Guiana", + "GG": "Guernsey", + "GH": "Ghana", + "GI": "Gibraltar", + "GL": "Greenland", + "GM": "Gambia", + "GN": "Guinea", + "GP": "Guadeloupe", + "GQ": "Equatorial Guinea", + "GR": "Greece", + "GS": "South Georgia and the South Sandwich Islands", + "GT": "Guatemala", + "GU": "Guam", + "GW": "Guinea-Bissau", + "GY": "Guyana", + "HK": "Hong Kong", + "HM": "Heard Island and McDonald Islands", + "HN": "Honduras", + "HR": "Croatia", + "HT": "Haiti", + "HU": "Hungary", + "ID": "Indonesia", + "IE": "Ireland", + "IL": "Israel", + "IM": "Isle of Man", + "IN": "India", + "IO": "British Indian Ocean Territory", + "IQ": "Iraq", + "IR": "Iran (Islamic Republic of)", + "IS": "Iceland", + "IT": "Italy", + "JE": "Jersey", + "JM": "Jamaica", + "JO": "Jordan", + "JP": "Japan", + "KE": "Kenya", + "KG": "Kyrgyzstan", + "KH": "Cambodia", + "KI": "Kiribati", + "KM": "Comoros", + "KN": "Saint Kitts and Nevis", + "KP": "Korea (the Democratic People's Republic of)", + "KR": "Korea (the Republic of)", + "KW": "Kuwait", + "KY": "Cayman Islands", + "KZ": "Kazakhstan", + "LA": "Lao People's Democratic Republic", + "LB": "Lebanon", + "LC": "Saint Lucia", + "LI": "Liechtenstein", + "LK": "Sri Lanka", + "LR": "Liberia", + "LS": "Lesotho", + "LT": "Lithuania", + "LU": "Luxembourg", + "LV": "Latvia", + "LY": "Libya", + "MA": "Morocco", + "MC": "Monaco", + "MD": "Moldova (the Republic of)", + "ME": "Montenegro", + "MF": "Saint Martin (French part)", + "MG": "Madagascar", + "MH": "Marshall Islands", + "MK": "North Macedonia", + "ML": "Mali", + "MM": "Myanmar", + "MN": "Mongolia", + "MO": "Macao", + "MP": "Northern Mariana Islands", + "MQ": "Martinique", + "MR": "Mauritania", + "MS": "Montserrat", + "MT": "Malta", + "MU": "Mauritius", + "MV": "Maldives", + "MW": "Malawi", + "MX": "Mexico", + "MY": "Malaysia", + "MZ": "Mozambique", + "NA": "Namibia", + "NC": "New Caledonia", + "NE": "Niger", + "NF": "Norfolk Island", + "NG": "Nigeria", + "NI": "Nicaragua", + "NL": "Netherlands", + "NO": "Norway", + "NP": "Nepal", + "NR": "Nauru", + "NU": "Niue", + "NZ": "New Zealand", + "OM": "Oman", + "PA": "Panama", + "PE": "Peru", + "PF": "French Polynesia", + "PG": "Papua New Guinea", + "PH": "Philippines", + "PK": "Pakistan", + "PL": "Poland", + "PM": "Saint Pierre and Miquelon", + "PN": "Pitcairn", + "PR": "Puerto Rico", + "PS": "Palestine, State of", + "PT": "Portugal", + "PW": "Palau", + "PY": "Paraguay", + "QA": "Qatar", + "RE": "Réunion", + "RO": "Romania", + "RS": "Serbia", + "RU": "Russian Federation", + "RW": "Rwanda", + "SA": "Saudi Arabia", + "SB": "Solomon Islands", + "SC": "Seychelles", + "SD": "Sudan", + "SE": "Sweden", + "SG": "Singapore", + "SH": "Saint Helena, Ascension and Tristan da Cunha", + "SI": "Slovenia", + "SJ": "Svalbard and Jan Mayen", + "SK": "Slovakia", + "SL": "Sierra Leone", + "SM": "San Marino", + "SN": "Senegal", + "SO": "Somalia", + "SR": "Suriname", + "SS": "South Sudan", + "ST": "Sao Tome and Principe", + "SV": "El Salvador", + "SX": "Sint Maarten (Dutch part)", + "SY": "Syrian Arab Republic", + "SZ": "Eswatini", + "TC": "Turks and Caicos Islands", + "TD": "Chad", + "TF": "French Southern Territories", + "TG": "Togo", + "TH": "Thailand", + "TJ": "Tajikistan", + "TK": "Tokelau", + "TL": "Timor-Leste", + "TM": "Turkmenistan", + "TN": "Tunisia", + "TO": "Tonga", + "TR": "Turkey", + "TT": "Trinidad and Tobago", + "TV": "Tuvalu", + "TW": "Taiwan", + "TZ": "Tanzania, the United Republic of", + "UA": "Ukraine", + "UG": "Uganda", + "UM": "United States Minor Outlying Islands", + "US": "United States", + "UY": "Uruguay", + "UZ": "Uzbekistan", + "VA": "Holy See", + "VC": "Saint Vincent and the Grenadines", + "VE": "Venezuela (Bolivarian Republic of)", + "VG": "Virgin Islands (British)", + "VI": "Virgin Islands (U.S.)", + "VN": "Viet Nam", + "VU": "Vanuatu", + "WF": "Wallis and Futuna", + "WS": "Samoa", + "YE": "Yemen", + "YT": "Mayotte", + "ZA": "South Africa", + "ZM": "Zambia", + "ZW": "Zimbabwe" +} \ No newline at end of file diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json new file mode 100644 index 000000000..5ada0eb7d --- /dev/null +++ b/src/assets/lang/en.json @@ -0,0 +1,304 @@ +{ + "assets.countries.AD": "Andorra", + "assets.countries.AE": "United Arab Emirates", + "assets.countries.AF": "Afghanistan", + "assets.countries.AG": "Antigua and Barbuda", + "assets.countries.AI": "Anguilla", + "assets.countries.AL": "Albania", + "assets.countries.AM": "Armenia", + "assets.countries.AO": "Angola", + "assets.countries.AQ": "Antarctica", + "assets.countries.AR": "Argentina", + "assets.countries.AS": "American Samoa", + "assets.countries.AT": "Austria", + "assets.countries.AU": "Australia", + "assets.countries.AW": "Aruba", + "assets.countries.AX": "Åland Islands", + "assets.countries.AZ": "Azerbaijan", + "assets.countries.BA": "Bosnia and Herzegovina", + "assets.countries.BB": "Barbados", + "assets.countries.BD": "Bangladesh", + "assets.countries.BE": "Belgium", + "assets.countries.BF": "Burkina Faso", + "assets.countries.BG": "Bulgaria", + "assets.countries.BH": "Bahrain", + "assets.countries.BI": "Burundi", + "assets.countries.BJ": "Benin", + "assets.countries.BL": "Saint Barthélemy", + "assets.countries.BM": "Bermuda", + "assets.countries.BN": "Brunei Darussalam", + "assets.countries.BO": "Bolivia (Plurinational State of)", + "assets.countries.BQ": "Bonaire, Sint Eustatius and Saba", + "assets.countries.BR": "Brazil", + "assets.countries.BS": "Bahamas", + "assets.countries.BT": "Bhutan", + "assets.countries.BV": "Bouvet Island", + "assets.countries.BW": "Botswana", + "assets.countries.BY": "Belarus", + "assets.countries.BZ": "Belize", + "assets.countries.CA": "Canada", + "assets.countries.CC": "Cocos (Keeling) Islands", + "assets.countries.CD": "Congo (the Democratic Republic of the)", + "assets.countries.CF": "Central African Republic", + "assets.countries.CG": "Congo", + "assets.countries.CH": "Switzerland", + "assets.countries.CI": "Côte d'Ivoire", + "assets.countries.CK": "Cook Islands", + "assets.countries.CL": "Chile", + "assets.countries.CM": "Cameroon", + "assets.countries.CN": "China", + "assets.countries.CO": "Colombia", + "assets.countries.CR": "Costa Rica", + "assets.countries.CU": "Cuba", + "assets.countries.CV": "Cabo Verde", + "assets.countries.CW": "Curaçao", + "assets.countries.CX": "Christmas Island", + "assets.countries.CY": "Cyprus", + "assets.countries.CZ": "Czechia", + "assets.countries.DE": "Germany", + "assets.countries.DJ": "Djibouti", + "assets.countries.DK": "Denmark", + "assets.countries.DM": "Dominica", + "assets.countries.DO": "Dominican Republic", + "assets.countries.DZ": "Algeria", + "assets.countries.EC": "Ecuador", + "assets.countries.EE": "Estonia", + "assets.countries.EG": "Egypt", + "assets.countries.EH": "Western Sahara", + "assets.countries.ER": "Eritrea", + "assets.countries.ES": "Spain", + "assets.countries.ET": "Ethiopia", + "assets.countries.FI": "Finland", + "assets.countries.FJ": "Fiji", + "assets.countries.FK": "Falkland Islands (Malvinas)", + "assets.countries.FM": "Micronesia (Federated States of)", + "assets.countries.FO": "Faroe Islands", + "assets.countries.FR": "France", + "assets.countries.GA": "Gabon", + "assets.countries.GB": "United Kingdom", + "assets.countries.GD": "Grenada", + "assets.countries.GE": "Georgia", + "assets.countries.GF": "French Guiana", + "assets.countries.GG": "Guernsey", + "assets.countries.GH": "Ghana", + "assets.countries.GI": "Gibraltar", + "assets.countries.GL": "Greenland", + "assets.countries.GM": "Gambia", + "assets.countries.GN": "Guinea", + "assets.countries.GP": "Guadeloupe", + "assets.countries.GQ": "Equatorial Guinea", + "assets.countries.GR": "Greece", + "assets.countries.GS": "South Georgia and the South Sandwich Islands", + "assets.countries.GT": "Guatemala", + "assets.countries.GU": "Guam", + "assets.countries.GW": "Guinea-Bissau", + "assets.countries.GY": "Guyana", + "assets.countries.HK": "Hong Kong", + "assets.countries.HM": "Heard Island and McDonald Islands", + "assets.countries.HN": "Honduras", + "assets.countries.HR": "Croatia", + "assets.countries.HT": "Haiti", + "assets.countries.HU": "Hungary", + "assets.countries.ID": "Indonesia", + "assets.countries.IE": "Ireland", + "assets.countries.IL": "Israel", + "assets.countries.IM": "Isle of Man", + "assets.countries.IN": "India", + "assets.countries.IO": "British Indian Ocean Territory", + "assets.countries.IQ": "Iraq", + "assets.countries.IR": "Iran (Islamic Republic of)", + "assets.countries.IS": "Iceland", + "assets.countries.IT": "Italy", + "assets.countries.JE": "Jersey", + "assets.countries.JM": "Jamaica", + "assets.countries.JO": "Jordan", + "assets.countries.JP": "Japan", + "assets.countries.KE": "Kenya", + "assets.countries.KG": "Kyrgyzstan", + "assets.countries.KH": "Cambodia", + "assets.countries.KI": "Kiribati", + "assets.countries.KM": "Comoros", + "assets.countries.KN": "Saint Kitts and Nevis", + "assets.countries.KP": "Korea (the Democratic People's Republic of)", + "assets.countries.KR": "Korea (the Republic of)", + "assets.countries.KW": "Kuwait", + "assets.countries.KY": "Cayman Islands", + "assets.countries.KZ": "Kazakhstan", + "assets.countries.LA": "Lao People's Democratic Republic", + "assets.countries.LB": "Lebanon", + "assets.countries.LC": "Saint Lucia", + "assets.countries.LI": "Liechtenstein", + "assets.countries.LK": "Sri Lanka", + "assets.countries.LR": "Liberia", + "assets.countries.LS": "Lesotho", + "assets.countries.LT": "Lithuania", + "assets.countries.LU": "Luxembourg", + "assets.countries.LV": "Latvia", + "assets.countries.LY": "Libya", + "assets.countries.MA": "Morocco", + "assets.countries.MC": "Monaco", + "assets.countries.MD": "Moldova (the Republic of)", + "assets.countries.ME": "Montenegro", + "assets.countries.MF": "Saint Martin (French part)", + "assets.countries.MG": "Madagascar", + "assets.countries.MH": "Marshall Islands", + "assets.countries.MK": "North Macedonia", + "assets.countries.ML": "Mali", + "assets.countries.MM": "Myanmar", + "assets.countries.MN": "Mongolia", + "assets.countries.MO": "Macao", + "assets.countries.MP": "Northern Mariana Islands", + "assets.countries.MQ": "Martinique", + "assets.countries.MR": "Mauritania", + "assets.countries.MS": "Montserrat", + "assets.countries.MT": "Malta", + "assets.countries.MU": "Mauritius", + "assets.countries.MV": "Maldives", + "assets.countries.MW": "Malawi", + "assets.countries.MX": "Mexico", + "assets.countries.MY": "Malaysia", + "assets.countries.MZ": "Mozambique", + "assets.countries.NA": "Namibia", + "assets.countries.NC": "New Caledonia", + "assets.countries.NE": "Niger", + "assets.countries.NF": "Norfolk Island", + "assets.countries.NG": "Nigeria", + "assets.countries.NI": "Nicaragua", + "assets.countries.NL": "Netherlands", + "assets.countries.NO": "Norway", + "assets.countries.NP": "Nepal", + "assets.countries.NR": "Nauru", + "assets.countries.NU": "Niue", + "assets.countries.NZ": "New Zealand", + "assets.countries.OM": "Oman", + "assets.countries.PA": "Panama", + "assets.countries.PE": "Peru", + "assets.countries.PF": "French Polynesia", + "assets.countries.PG": "Papua New Guinea", + "assets.countries.PH": "Philippines", + "assets.countries.PK": "Pakistan", + "assets.countries.PL": "Poland", + "assets.countries.PM": "Saint Pierre and Miquelon", + "assets.countries.PN": "Pitcairn", + "assets.countries.PR": "Puerto Rico", + "assets.countries.PS": "Palestine, State of", + "assets.countries.PT": "Portugal", + "assets.countries.PW": "Palau", + "assets.countries.PY": "Paraguay", + "assets.countries.QA": "Qatar", + "assets.countries.RE": "Réunion", + "assets.countries.RO": "Romania", + "assets.countries.RS": "Serbia", + "assets.countries.RU": "Russian Federation", + "assets.countries.RW": "Rwanda", + "assets.countries.SA": "Saudi Arabia", + "assets.countries.SB": "Solomon Islands", + "assets.countries.SC": "Seychelles", + "assets.countries.SD": "Sudan", + "assets.countries.SE": "Sweden", + "assets.countries.SG": "Singapore", + "assets.countries.SH": "Saint Helena, Ascension and Tristan da Cunha", + "assets.countries.SI": "Slovenia", + "assets.countries.SJ": "Svalbard and Jan Mayen", + "assets.countries.SK": "Slovakia", + "assets.countries.SL": "Sierra Leone", + "assets.countries.SM": "San Marino", + "assets.countries.SN": "Senegal", + "assets.countries.SO": "Somalia", + "assets.countries.SR": "Suriname", + "assets.countries.SS": "South Sudan", + "assets.countries.ST": "Sao Tome and Principe", + "assets.countries.SV": "El Salvador", + "assets.countries.SX": "Sint Maarten (Dutch part)", + "assets.countries.SY": "Syrian Arab Republic", + "assets.countries.SZ": "Eswatini", + "assets.countries.TC": "Turks and Caicos Islands", + "assets.countries.TD": "Chad", + "assets.countries.TF": "French Southern Territories", + "assets.countries.TG": "Togo", + "assets.countries.TH": "Thailand", + "assets.countries.TJ": "Tajikistan", + "assets.countries.TK": "Tokelau", + "assets.countries.TL": "Timor-Leste", + "assets.countries.TM": "Turkmenistan", + "assets.countries.TN": "Tunisia", + "assets.countries.TO": "Tonga", + "assets.countries.TR": "Turkey", + "assets.countries.TT": "Trinidad and Tobago", + "assets.countries.TV": "Tuvalu", + "assets.countries.TW": "Taiwan", + "assets.countries.TZ": "Tanzania, the United Republic of", + "assets.countries.UA": "Ukraine", + "assets.countries.UG": "Uganda", + "assets.countries.UM": "United States Minor Outlying Islands", + "assets.countries.US": "United States", + "assets.countries.UY": "Uruguay", + "assets.countries.UZ": "Uzbekistan", + "assets.countries.VA": "Holy See", + "assets.countries.VC": "Saint Vincent and the Grenadines", + "assets.countries.VE": "Venezuela (Bolivarian Republic of)", + "assets.countries.VG": "Virgin Islands (British)", + "assets.countries.VI": "Virgin Islands (U.S.)", + "assets.countries.VN": "Viet Nam", + "assets.countries.VU": "Vanuatu", + "assets.countries.WF": "Wallis and Futuna", + "assets.countries.WS": "Samoa", + "assets.countries.YE": "Yemen", + "assets.countries.YT": "Mayotte", + "assets.countries.ZA": "South Africa", + "assets.countries.ZM": "Zambia", + "assets.countries.ZW": "Zimbabwe", + "assets.mimetypes.application/epub_zip": "EPUB ebook", + "assets.mimetypes.application/msword": "Word document", + "assets.mimetypes.application/pdf": "PDF document", + "assets.mimetypes.application/vnd.moodle.backup": "Moodle backup", + "assets.mimetypes.application/vnd.ms-excel": "Excel spreadsheet", + "assets.mimetypes.application/vnd.ms-excel.sheet.macroEnabled.12": "Excel 2007 macro-enabled workbook", + "assets.mimetypes.application/vnd.ms-powerpoint": "Powerpoint presentation", + "assets.mimetypes.application/vnd.oasis.opendocument.spreadsheet": "OpenDocument Spreadsheet", + "assets.mimetypes.application/vnd.oasis.opendocument.spreadsheet-template": "OpenDocument Spreadsheet template", + "assets.mimetypes.application/vnd.oasis.opendocument.text": "OpenDocument Text document", + "assets.mimetypes.application/vnd.oasis.opendocument.text-template": "OpenDocument Text template", + "assets.mimetypes.application/vnd.oasis.opendocument.text-web": "OpenDocument Web page template", + "assets.mimetypes.application/vnd.openxmlformats-officedocument.presentationml.presentation": "Powerpoint 2007 presentation", + "assets.mimetypes.application/vnd.openxmlformats-officedocument.presentationml.slideshow": "Powerpoint 2007 slideshow", + "assets.mimetypes.application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "Excel 2007 spreadsheet", + "assets.mimetypes.application/vnd.openxmlformats-officedocument.spreadsheetml.template": "Excel 2007 template", + "assets.mimetypes.application/vnd.openxmlformats-officedocument.wordprocessingml.document": "Word 2007 document", + "assets.mimetypes.application/x-iwork-keynote-sffkey": "iWork Keynote presentation", + "assets.mimetypes.application/x-iwork-numbers-sffnumbers": "iWork Numbers spreadsheet", + "assets.mimetypes.application/x-iwork-pages-sffpages": "iWork Pages document", + "assets.mimetypes.application/x-javascript": "JavaScript source", + "assets.mimetypes.application/x-mspublisher": "Publisher document", + "assets.mimetypes.application/x-shockwave-flash": "Flash animation", + "assets.mimetypes.application/xhtml_xml": "XHTML document", + "assets.mimetypes.archive": "Archive ({{$a.EXT}})", + "assets.mimetypes.audio": "Audio file ({{$a.EXT}})", + "assets.mimetypes.default": "{{$a.mimetype}}", + "assets.mimetypes.document/unknown": "File", + "assets.mimetypes.group:archive": "Archive files", + "assets.mimetypes.group:audio": "Audio files", + "assets.mimetypes.group:document": "Document files", + "assets.mimetypes.group:html_audio": "Audio files natively supported by browsers", + "assets.mimetypes.group:html_track": "HTML track files", + "assets.mimetypes.group:html_video": "Video files natively supported by browsers", + "assets.mimetypes.group:image": "Image files", + "assets.mimetypes.group:presentation": "Presentation files", + "assets.mimetypes.group:sourcecode": "Source code", + "assets.mimetypes.group:spreadsheet": "Spreadsheet files", + "assets.mimetypes.group:video": "Video files", + "assets.mimetypes.group:web_audio": "Audio files used on the web", + "assets.mimetypes.group:web_file": "Web files", + "assets.mimetypes.group:web_image": "Image files used on the web", + "assets.mimetypes.group:web_video": "Video files used on the web", + "assets.mimetypes.image": "Image ({{$a.MIMETYPE2}})", + "assets.mimetypes.image/vnd.microsoft.icon": "Windows icon", + "assets.mimetypes.text/css": "Cascading Style-Sheet", + "assets.mimetypes.text/csv": "Comma-separated values", + "assets.mimetypes.text/html": "HTML document", + "assets.mimetypes.text/plain": "Text file", + "assets.mimetypes.text/rtf": "RTF document", + "assets.mimetypes.text/vtt": "Web Video Text Track", + "assets.mimetypes.video": "Video file ({{$a.EXT}})", + "core.login.yourenteredsite": "Connect to your site" +} \ No newline at end of file diff --git a/src/assets/mimetypes/en.json b/src/assets/mimetypes/en.json new file mode 100644 index 000000000..f94347be4 --- /dev/null +++ b/src/assets/mimetypes/en.json @@ -0,0 +1,54 @@ +{ + "application/epub_zip": "EPUB ebook", + "application/msword": "Word document", + "application/pdf": "PDF document", + "application/vnd.moodle.backup": "Moodle backup", + "application/vnd.ms-excel": "Excel spreadsheet", + "application/vnd.ms-excel.sheet.macroEnabled.12": "Excel 2007 macro-enabled workbook", + "application/vnd.ms-powerpoint": "Powerpoint presentation", + "application/vnd.oasis.opendocument.spreadsheet": "OpenDocument Spreadsheet", + "application/vnd.oasis.opendocument.spreadsheet-template": "OpenDocument Spreadsheet template", + "application/vnd.oasis.opendocument.text": "OpenDocument Text document", + "application/vnd.oasis.opendocument.text-template": "OpenDocument Text template", + "application/vnd.oasis.opendocument.text-web": "OpenDocument Web page template", + "application/vnd.openxmlformats-officedocument.presentationml.presentation": "Powerpoint 2007 presentation", + "application/vnd.openxmlformats-officedocument.presentationml.slideshow": "Powerpoint 2007 slideshow", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "Excel 2007 spreadsheet", + "application/vnd.openxmlformats-officedocument.spreadsheetml.template": "Excel 2007 template", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "Word 2007 document", + "application/x-iwork-keynote-sffkey": "iWork Keynote presentation", + "application/x-iwork-numbers-sffnumbers": "iWork Numbers spreadsheet", + "application/x-iwork-pages-sffpages": "iWork Pages document", + "application/x-javascript": "JavaScript source", + "application/x-mspublisher": "Publisher document", + "application/x-shockwave-flash": "Flash animation", + "application/xhtml_xml": "XHTML document", + "archive": "Archive ({{$a.EXT}})", + "audio": "Audio file ({{$a.EXT}})", + "default": "{{$a.mimetype}}", + "document/unknown": "File", + "group:archive": "Archive files", + "group:audio": "Audio files", + "group:document": "Document files", + "group:html_audio": "Audio files natively supported by browsers", + "group:html_track": "HTML track files", + "group:html_video": "Video files natively supported by browsers", + "group:image": "Image files", + "group:presentation": "Presentation files", + "group:sourcecode": "Source code", + "group:spreadsheet": "Spreadsheet files", + "group:video": "Video files", + "group:web_audio": "Audio files used on the web", + "group:web_file": "Web files", + "group:web_image": "Image files used on the web", + "group:web_video": "Video files used on the web", + "image": "Image ({{$a.MIMETYPE2}})", + "image/vnd.microsoft.icon": "Windows icon", + "text/css": "Cascading Style-Sheet", + "text/csv": "Comma-separated values", + "text/html": "HTML document", + "text/plain": "Text file", + "text/rtf": "RTF document", + "text/vtt": "Web Video Text Track", + "video": "Video file ({{$a.EXT}})" +} \ No newline at end of file