From 658304cc51eca3a5250d81e55b9a3e27a210cecd Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 23 Apr 2021 16:56:17 +0100 Subject: [PATCH 01/15] Wrap all strings in _td function for i18n --- package.json | 4 + scripts/gen-i18n.js | 304 ++++++++++++++++++++++++++++++++++++ src/electron-main.js | 6 +- src/i18n/strings/en_EN.json | 30 ++++ src/language-helper.js | 11 ++ src/tray.js | 5 +- src/vectormenu.js | 30 ++-- src/webcontents-handler.js | 31 ++-- 8 files changed, 394 insertions(+), 27 deletions(-) create mode 100644 scripts/gen-i18n.js create mode 100644 src/i18n/strings/en_EN.json create mode 100644 src/language-helper.js diff --git a/package.json b/package.json index aa240b4..b19a79d 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,11 @@ }, "license": "Apache-2.0", "files": [], + "bin": { + "matrix-gen-i18n": "scripts/gen-i18n.js" + }, "scripts": { + "i18n": "matrix-gen-i18n", "mkdirs": "mkdirp packages deploys", "fetch": "yarn run mkdirs && node scripts/fetch-package.js", "asar-webapp": "asar p webapp webapp.asar", diff --git a/scripts/gen-i18n.js b/scripts/gen-i18n.js new file mode 100644 index 0000000..2c718a7 --- /dev/null +++ b/scripts/gen-i18n.js @@ -0,0 +1,304 @@ +#!/usr/bin/env node + +/* +Copyright 2017 New Vector 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. +*/ + +/** + * Regenerates the translations en_EN file by walking the source tree and + * parsing each file with the appropriate parser. Emits a JSON file with the + * translatable strings mapped to themselves in the order they appeared + * in the files and grouped by the file they appeared in. + * + * Usage: node scripts/gen-i18n.js + */ +const fs = require('fs'); +const path = require('path'); + +const walk = require('walk'); + +const parser = require("@babel/parser"); +const traverse = require("@babel/traverse"); + +const TRANSLATIONS_FUNCS = ['_t', '_td']; + +const INPUT_TRANSLATIONS_FILE = 'src/i18n/strings/en_EN.json'; +const OUTPUT_FILE = 'src/i18n/strings/en_EN.json'; + +// NB. The sync version of walk is broken for single files so we walk +// all of res rather than just res/home.html. +// https://git.daplie.com/Daplie/node-walk/merge_requests/1 fixes it, +// or if we get bored waiting for it to be merged, we could switch +// to a project that's actively maintained. +const SEARCH_PATHS = ['src', 'res']; + +function getObjectValue(obj, key) { + for (const prop of obj.properties) { + if (prop.key.type === 'Identifier' && prop.key.name === key) { + return prop.value; + } + } + return null; +} + +function getTKey(arg) { + if (arg.type === 'Literal' || arg.type === "StringLiteral") { + return arg.value; + } else if (arg.type === 'BinaryExpression' && arg.operator === '+') { + return getTKey(arg.left) + getTKey(arg.right); + } else if (arg.type === 'TemplateLiteral') { + return arg.quasis.map((q) => { + return q.value.raw; + }).join(''); + } + return null; +} + +function getFormatStrings(str) { + // Match anything that starts with % + // We could make a regex that matched the full placeholder, but this + // would just not match invalid placeholders and so wouldn't help us + // detect the invalid ones. + // Also note that for simplicity, this just matches a % character and then + // anything up to the next % character (or a single %, or end of string). + const formatStringRe = /%([^%]+|%|$)/g; + const formatStrings = new Set(); + + let match; + while ( (match = formatStringRe.exec(str)) !== null ) { + const placeholder = match[1]; // Minus the leading '%' + if (placeholder === '%') continue; // Literal % is %% + + const placeholderMatch = placeholder.match(/^\((.*?)\)(.)/); + if (placeholderMatch === null) { + throw new Error("Invalid format specifier: '"+match[0]+"'"); + } + if (placeholderMatch.length < 3) { + throw new Error("Malformed format specifier"); + } + const placeholderName = placeholderMatch[1]; + const placeholderFormat = placeholderMatch[2]; + + if (placeholderFormat !== 's') { + throw new Error(`'${placeholderFormat}' used as format character: you probably meant 's'`); + } + + formatStrings.add(placeholderName); + } + + return formatStrings; +} + +function getTranslationsJs(file) { + const contents = fs.readFileSync(file, { encoding: 'utf8' }); + + const trs = new Set(); + + try { + const plugins = [ + // https://babeljs.io/docs/en/babel-parser#plugins + "classProperties", + "objectRestSpread", + "throwExpressions", + "exportDefaultFrom", + "decorators-legacy", + ]; + + if (file.endsWith(".js") || file.endsWith(".jsx")) { + // all JS is assumed to be flow or react + plugins.push("flow", "jsx"); + } else if (file.endsWith(".ts")) { + // TS can't use JSX unless it's a TSX file (otherwise angle casts fail) + plugins.push("typescript"); + } else if (file.endsWith(".tsx")) { + // When the file is a TSX file though, enable JSX parsing + plugins.push("typescript", "jsx"); + } + + const babelParsed = parser.parse(contents, { + allowImportExportEverywhere: true, + errorRecovery: true, + sourceFilename: file, + tokens: true, + plugins, + }); + traverse.default(babelParsed, { + enter: (p) => { + const node = p.node; + if (p.isCallExpression() && node.callee && TRANSLATIONS_FUNCS.includes(node.callee.name)) { + const tKey = getTKey(node.arguments[0]); + + // This happens whenever we call _t with non-literals (ie. whenever we've + // had to use a _td to compensate) so is expected. + if (tKey === null) return; + + // check the format string against the args + // We only check _t: _td has no args + if (node.callee.name === '_t') { + try { + const placeholders = getFormatStrings(tKey); + for (const placeholder of placeholders) { + if (node.arguments.length < 2) { + throw new Error(`Placeholder found ('${placeholder}') but no substitutions given`); + } + const value = getObjectValue(node.arguments[1], placeholder); + if (value === null) { + throw new Error(`No value found for placeholder '${placeholder}'`); + } + } + + // Validate tag replacements + if (node.arguments.length > 2) { + const tagMap = node.arguments[2]; + for (const prop of tagMap.properties || []) { + if (prop.key.type === 'Literal') { + const tag = prop.key.value; + // RegExp same as in src/languageHandler.js + const regexp = new RegExp(`(<${tag}>(.*?)<\\/${tag}>|<${tag}>|<${tag}\\s*\\/>)`); + if (!tKey.match(regexp)) { + throw new Error(`No match for ${regexp} in ${tKey}`); + } + } + } + } + } catch (e) { + console.log(); + console.error(`ERROR: ${file}:${node.loc.start.line} ${tKey}`); + console.error(e); + process.exit(1); + } + } + + let isPlural = false; + if (node.arguments.length > 1 && node.arguments[1].type === 'ObjectExpression') { + const countVal = getObjectValue(node.arguments[1], 'count'); + if (countVal) { + isPlural = true; + } + } + + if (isPlural) { + trs.add(tKey + "|other"); + const plurals = enPlurals[tKey]; + if (plurals) { + for (const pluralType of Object.keys(plurals)) { + trs.add(tKey + "|" + pluralType); + } + } + } else { + trs.add(tKey); + } + } + }, + }); + } catch (e) { + console.error(e); + process.exit(1); + } + + return trs; +} + +function getTranslationsOther(file) { + const contents = fs.readFileSync(file, { encoding: 'utf8' }); + + const trs = new Set(); + + // Taken from element-web src/components/structures/HomePage.js + const translationsRegex = /_t\(['"]([\s\S]*?)['"]\)/mg; + let matches; + while (matches = translationsRegex.exec(contents)) { + trs.add(matches[1]); + } + return trs; +} + +// gather en_EN plural strings from the input translations file: +// the en_EN strings are all in the source with the exception of +// pluralised strings, which we need to pull in from elsewhere. +const inputTranslationsRaw = JSON.parse(fs.readFileSync(INPUT_TRANSLATIONS_FILE, { encoding: 'utf8' })); +const enPlurals = {}; + +for (const key of Object.keys(inputTranslationsRaw)) { + const parts = key.split("|"); + if (parts.length > 1) { + const plurals = enPlurals[parts[0]] || {}; + plurals[parts[1]] = inputTranslationsRaw[key]; + enPlurals[parts[0]] = plurals; + } +} + +const translatables = new Set(); + +const walkOpts = { + listeners: { + names: function(root, nodeNamesArray) { + // Sort the names case insensitively and alphabetically to + // maintain some sense of order between the different strings. + nodeNamesArray.sort((a, b) => { + a = a.toLowerCase(); + b = b.toLowerCase(); + if (a > b) return 1; + if (a < b) return -1; + return 0; + }); + }, + file: function(root, fileStats, next) { + const fullPath = path.join(root, fileStats.name); + + let trs; + if (fileStats.name.endsWith('.js') || fileStats.name.endsWith('.ts') || fileStats.name.endsWith('.tsx')) { + trs = getTranslationsJs(fullPath); + } else if (fileStats.name.endsWith('.html')) { + trs = getTranslationsOther(fullPath); + } else { + return; + } + console.log(`${fullPath} (${trs.size} strings)`); + for (const tr of trs.values()) { + // Convert DOS line endings to unix + translatables.add(tr.replace(/\r\n/g, "\n")); + } + }, + }, +}; + +for (const path of SEARCH_PATHS) { + if (fs.existsSync(path)) { + walk.walkSync(path, walkOpts); + } +} + +const trObj = {}; +for (const tr of translatables) { + if (tr.includes("|")) { + if (inputTranslationsRaw[tr]) { + trObj[tr] = inputTranslationsRaw[tr]; + } else { + trObj[tr] = tr.split("|")[0]; + } + } else { + trObj[tr] = tr; + } +} + +fs.writeFileSync( + OUTPUT_FILE, + JSON.stringify(trObj, translatables.values(), 4) + "\n", +); + +console.log(); +console.log(`Wrote ${translatables.size} strings to ${OUTPUT_FILE}`); + diff --git a/src/electron-main.js b/src/electron-main.js index 47c4e19..d60e01e 100644 --- a/src/electron-main.js +++ b/src/electron-main.js @@ -57,6 +57,8 @@ try { } } +const { _td } = require('./language-helper'); + let seshatSupported = false; let Seshat; let SeshatRecovery; @@ -268,8 +270,8 @@ const warnBeforeExit = (event, input) => { if (shouldWarnBeforeExit && exitShortcutPressed) { const shouldCancelCloseRequest = dialog.showMessageBoxSync(mainWindow, { type: "question", - buttons: ["Cancel", "Close Element"], - message: "Are you sure you want to quit?", + buttons: [_td("Cancel"), _td("Close Element")], + message: _td("Are you sure you want to quit?"), defaultId: 1, cancelId: 0, }) === 0; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json new file mode 100644 index 0000000..a6cd410 --- /dev/null +++ b/src/i18n/strings/en_EN.json @@ -0,0 +1,30 @@ +{ + "Cancel": "Cancel", + "Close Element": "Close Element", + "Are you sure you want to quit?": "Are you sure you want to quit?", + "Show/Hide": "Show/Hide", + "Quit": "Quit", + "Edit": "Edit", + "View": "View", + "Preferences": "Preferences", + "Window": "Window", + "Help": "Help", + "Element Help": "Element Help", + "Speech": "Speech", + "Close": "Close", + "Minimize": "Minimize", + "Zoom": "Zoom", + "Bring All to Front": "Bring All to Front", + "File": "File", + "Copy image": "Copy image", + "Copy email address": "Copy email address", + "Copy link address": "Copy link address", + "Save image as...": "Save image as...", + "Failed to save image": "Failed to save image", + "The image failed to save": "The image failed to save", + "Add to dictionary": "Add to dictionary", + "Cut": "Cut", + "Copy": "Copy", + "Paste": "Paste", + "Select All": "Select All" +} diff --git a/src/language-helper.js b/src/language-helper.js new file mode 100644 index 0000000..8fb03eb --- /dev/null +++ b/src/language-helper.js @@ -0,0 +1,11 @@ +// Function which only purpose is to mark that a string is translatable +// Does not actually do anything. It's helpful for automatic extraction of translatable strings + +function _td(s) { + return s; +} + + +module.exports = { + _td, +}; diff --git a/src/tray.js b/src/tray.js index e235e77..86e75ba 100644 --- a/src/tray.js +++ b/src/tray.js @@ -19,6 +19,7 @@ const {app, Tray, Menu, nativeImage} = require('electron'); const pngToIco = require('png-to-ico'); const path = require('path'); const fs = require('fs'); +const { _td } = require('./language-helper'); let trayIcon = null; @@ -49,12 +50,12 @@ exports.create = function(config) { const contextMenu = Menu.buildFromTemplate([ { - label: `Show/Hide ${config.brand}`, + label: _td('Show/Hide'), click: toggleWin, }, { type: 'separator' }, { - label: 'Quit', + label: _td('Quit'), click: function() { app.quit(); }, diff --git a/src/vectormenu.js b/src/vectormenu.js index 66fa573..1171d00 100644 --- a/src/vectormenu.js +++ b/src/vectormenu.js @@ -15,11 +15,13 @@ limitations under the License. */ const {app, shell, Menu} = require('electron'); +const { _td } = require('./language-helper'); // Menu template from http://electron.atom.io/docs/api/menu/, edited const template = [ { - label: '&Edit', + label: _td('Edit'), + accelerator: 'e', submenu: [ { role: 'undo' }, { role: 'redo' }, @@ -33,7 +35,8 @@ const template = [ ], }, { - label: '&View', + label: _td('View'), + accelerator: 'V', submenu: [ { type: 'separator' }, { role: 'resetzoom' }, @@ -41,7 +44,7 @@ const template = [ { role: 'zoomout' }, { type: 'separator' }, { - label: 'Preferences', + label: _td('Preferences'), accelerator: 'Command+,', // Mac-only accelerator click() { global.mainWindow.webContents.send('preferences'); }, }, @@ -50,7 +53,8 @@ const template = [ ], }, { - label: '&Window', + label: _td('Window'), + accelerator: 'w', role: 'window', submenu: [ { role: 'minimize' }, @@ -58,11 +62,12 @@ const template = [ ], }, { - label: '&Help', + label: _td('Help'), + accelerator: 'h', role: 'help', submenu: [ { - label: 'Element Help', + label: _td('Element Help'), click() { shell.openExternal('https://element.io/help'); }, }, ], @@ -94,7 +99,7 @@ if (process.platform === 'darwin') { template[1].submenu.push( { type: 'separator' }, { - label: 'Speech', + label: _td('Speech'), submenu: [ { role: 'startspeaking' }, { role: 'stopspeaking' }, @@ -105,30 +110,31 @@ if (process.platform === 'darwin') { // This also has specific functionality on macOS template[3].submenu = [ { - label: 'Close', + label: _td('Close'), accelerator: 'CmdOrCtrl+W', role: 'close', }, { - label: 'Minimize', + label: _td('Minimize'), accelerator: 'CmdOrCtrl+M', role: 'minimize', }, { - label: 'Zoom', + label: _td('Zoom'), role: 'zoom', }, { type: 'separator', }, { - label: 'Bring All to Front', + label: _td('Bring All to Front'), role: 'front', }, ]; } else { template.unshift({ - label: '&File', + label: _td('File'), + accelerator: 'f', submenu: [ // For some reason, 'about' does not seem to work on windows. /*{ diff --git a/src/webcontents-handler.js b/src/webcontents-handler.js index 3201772..a532743 100644 --- a/src/webcontents-handler.js +++ b/src/webcontents-handler.js @@ -3,6 +3,7 @@ const url = require('url'); const fs = require('fs'); const request = require('request'); const path = require('path'); +const { _td } = require('./language-helper'); const MAILTO_PREFIX = "mailto:"; @@ -73,7 +74,8 @@ function onLinkContextMenu(ev, params) { if (params.hasImageContents) { popupMenu.append(new MenuItem({ - label: '&Copy image', + label: _td('Copy image'), + accelerator: 'c', click() { ev.sender.copyImageAt(params.x, params.y); }, @@ -85,14 +87,16 @@ function onLinkContextMenu(ev, params) { // Special-case e-mail URLs to strip the `mailto:` like modern browsers do if (url.startsWith(MAILTO_PREFIX)) { popupMenu.append(new MenuItem({ - label: 'Copy email &address', + label: _td('Copy email address'), + accelerator: 'a', click() { clipboard.writeText(url.substr(MAILTO_PREFIX.length)); }, })); } else { popupMenu.append(new MenuItem({ - label: 'Copy link &address', + label: _td('Copy link address'), + accelerator: 'a', click() { clipboard.writeText(url); }, @@ -104,7 +108,8 @@ function onLinkContextMenu(ev, params) { // only the renderer can resolve them so don't give the user an option to. if (params.hasImageContents && !url.startsWith('blob:')) { popupMenu.append(new MenuItem({ - label: 'Sa&ve image as...', + label: _td('Save image as...'), + accelerator: 'a', async click() { const targetFileName = params.titleText || "image.png"; const {filePath} = await dialog.showSaveDialog({ @@ -123,8 +128,8 @@ function onLinkContextMenu(ev, params) { console.error(err); dialog.showMessageBox({ type: "error", - title: "Failed to save image", - message: "The image failed to save", + title: _td("Failed to save image"), + message: _td("The image failed to save"), }); } }, @@ -151,7 +156,7 @@ function _CutCopyPasteSelectContextMenus(params) { options.push({ type: 'separator', }, { - label: 'Add to dictionary', + label: _td('Add to dictionary'), click: (menuItem, browserWindow) => { browserWindow.webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord); }, @@ -162,22 +167,26 @@ function _CutCopyPasteSelectContextMenus(params) { options.push({ role: 'cut', - label: 'Cu&t', + label: _td('Cut'), + accelerator: 't', enabled: params.editFlags.canCut, }, { role: 'copy', - label: '&Copy', + label: _td('Copy'), + accelerator: 'c', enabled: params.editFlags.canCopy, }, { role: 'paste', - label: '&Paste', + label: _td('Paste'), + accelerator: 'p', enabled: params.editFlags.canPaste, }, { role: 'pasteandmatchstyle', enabled: params.editFlags.canPaste, }, { role: 'selectall', - label: "Select &All", + label: _td("Select All"), + accelerator: 'a', enabled: params.editFlags.canSelectAll, }); return options; From 105070716effc78248e8a225866b7f407d359e42 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 26 Apr 2021 13:58:29 +0100 Subject: [PATCH 02/15] Make application react to element-web language change --- package.json | 1 + src/electron-main.js | 17 ++- src/language-helper.js | 110 ++++++++++++++++++- src/tray.js | 61 ++++++----- src/vectormenu.js | 236 +++++++++++++++++++++-------------------- yarn.lock | 40 ++++++- 6 files changed, 315 insertions(+), 150 deletions(-) diff --git a/package.json b/package.json index b19a79d..d651e48 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ }, "dependencies": { "auto-launch": "^5.0.5", + "counterpart": "^0.18.6", "electron-store": "^6.0.1", "electron-window-state": "^5.0.3", "minimist": "^1.2.3", diff --git a/src/electron-main.js b/src/electron-main.js index d60e01e..82d0c5b 100644 --- a/src/electron-main.js +++ b/src/electron-main.js @@ -34,7 +34,7 @@ const AutoLaunch = require('auto-launch'); const path = require('path'); const tray = require('./tray'); -const vectorMenu = require('./vectormenu'); +const buildMenuTemplate = require('./vectormenu'); const webContentsHandler = require('./webcontents-handler'); const updater = require('./updater'); const {getProfileFromDeeplink, protocolInit, recordSSOSession} = require('./protocol'); @@ -57,7 +57,7 @@ try { } } -const { _td } = require('./language-helper'); +const { _td, AppLocalization } = require('./language-helper'); let seshatSupported = false; let Seshat; @@ -88,6 +88,7 @@ let vectorConfig; let iconPath; let trayConfig; let launcher; +let appLocalization; if (argv["help"]) { console.log("Options:"); @@ -368,6 +369,9 @@ ipcMain.on('ipcCall', async function(ev, payload) { launcher.disable(); } break; + case 'setLanguage': + appLocalization.setAppLocale(args[0]); + break; case 'shouldWarnBeforeExit': ret = store.get('warnBeforeExit', true); break; @@ -942,7 +946,6 @@ app.on('ready', async () => { }, }); mainWindow.loadURL('vector://vector/webapp/'); - Menu.setApplicationMenu(vectorMenu); // Handle spellchecker // For some reason spellCheckerEnabled isn't persisted so we have to use the store here @@ -991,6 +994,14 @@ app.on('ready', async () => { } webContentsHandler(mainWindow.webContents); + + appLocalization = new AppLocalization({ + store, + components: [ + () => tray.initApplicationMenu(), + () => Menu.setApplicationMenu(buildMenuTemplate()), + ], + }); }); app.on('window-all-closed', () => { diff --git a/src/language-helper.js b/src/language-helper.js index 8fb03eb..58bb6c9 100644 --- a/src/language-helper.js +++ b/src/language-helper.js @@ -1,11 +1,113 @@ -// Function which only purpose is to mark that a string is translatable -// Does not actually do anything. It's helpful for automatic extraction of translatable strings +const counterpart = require('counterpart'); -function _td(s) { - return s; +const DEFAULT_LOCALE = "en"; + +function _td(text) { + return _t(text); +} + +function _t(text, variables = {}) { + const args = Object.assign({ interpolate: false }, variables); + + const { count } = args; + + // Horrible hack to avoid https://github.com/vector-im/element-web/issues/4191 + // The interpolation library that counterpart uses does not support undefined/null + // values and instead will throw an error. This is a problem since everywhere else + // in JS land passing undefined/null will simply stringify instead, and when converting + // valid ES6 template strings to i18n strings it's extremely easy to pass undefined/null + // if there are no existing null guards. To avoid this making the app completely inoperable, + // we'll check all the values for undefined/null and stringify them here. + Object.keys(args).forEach((key) => { + if (args[key] === undefined) { + console.warn("safeCounterpartTranslate called with undefined interpolation name: " + key); + args[key] = 'undefined'; + } + if (args[key] === null) { + console.warn("safeCounterpartTranslate called with null interpolation name: " + key); + args[key] = 'null'; + } + }); + let translated = counterpart.translate(text, args); + if (translated === undefined && count !== undefined) { + // counterpart does not do fallback if no pluralisation exists + // in the preferred language, so do it here + translated = counterpart.translate(text, Object.assign({}, args, {locale: DEFAULT_LOCALE})); + } + + // The translation returns text so there's no XSS vector here (no unsafe HTML, no code execution) + return translated; +} + +class AppLocalization { + static STORE_KEY = "locale" + #store = null + + constructor({ store, components = [] }) { + counterpart.registerTranslations("en", this.fetchTranslationJson("en_EN")); + counterpart.setFallbackLocale('en'); + counterpart.setSeparator('|'); + + if (Array.isArray(components)) { + this.localizedComponents = new Set(components); + } + + this.#store = store; + if (this.#store.has(AppLocalization.STORE_KEY)) { + const locales = this.#store.get(AppLocalization.STORE_KEY); + this.setAppLocale(locales); + } + + this.resetLocalizedUI(); + } + + fetchTranslationJson(locale) { + try { + console.log("Fetching translation json for locale: " + locale); + return require(`./i18n/strings/${locale}.json`); + } catch (e) { + console.log(`Could not fetch translation json for locale: '${locale}'`, e); + return null; + } + } + + get languageTranslationJson() { + return this.translationJsonMap.get(this.language); + } + + setAppLocale(locales) { + console.log(`Changing application language to ${locales}`); + + if (!Array.isArray(locales)) { + locales = [locales]; + } + + locales.forEach(locale => { + const translations = this.fetchTranslationJson(locale); + if (translations !== null) { + counterpart.registerTranslations(locale, translations); + } + }); + + counterpart.setLocale(locales); + this.#store.set(AppLocalization.STORE_KEY, locales); + + this.resetLocalizedUI(); + } + + resetLocalizedUI() { + console.log("Resetting the UI components after locale change"); + this.localizedComponents.forEach(componentSetup => { + if (typeof componentSetup === "function") { + componentSetup(); + } + }); + } } module.exports = { + AppLocalization, + _t, _td, }; diff --git a/src/tray.js b/src/tray.js index 86e75ba..8e03462 100644 --- a/src/tray.js +++ b/src/tray.js @@ -34,39 +34,24 @@ exports.destroy = function() { } }; +const toggleWin = function() { + if (global.mainWindow.isVisible() && !global.mainWindow.isMinimized()) { + global.mainWindow.hide(); + } else { + if (global.mainWindow.isMinimized()) global.mainWindow.restore(); + if (!global.mainWindow.isVisible()) global.mainWindow.show(); + global.mainWindow.focus(); + } +}; + exports.create = function(config) { // no trays on darwin if (process.platform === 'darwin' || trayIcon) return; - - const toggleWin = function() { - if (global.mainWindow.isVisible() && !global.mainWindow.isMinimized()) { - global.mainWindow.hide(); - } else { - if (global.mainWindow.isMinimized()) global.mainWindow.restore(); - if (!global.mainWindow.isVisible()) global.mainWindow.show(); - global.mainWindow.focus(); - } - }; - - const contextMenu = Menu.buildFromTemplate([ - { - label: _td('Show/Hide'), - click: toggleWin, - }, - { type: 'separator' }, - { - label: _td('Quit'), - click: function() { - app.quit(); - }, - }, - ]); - const defaultIcon = nativeImage.createFromPath(config.icon_path); trayIcon = new Tray(defaultIcon); trayIcon.setToolTip(config.brand); - trayIcon.setContextMenu(contextMenu); + initApplicationMenu(); trayIcon.on('click', toggleWin); let lastFavicon = null; @@ -105,3 +90,27 @@ exports.create = function(config) { trayIcon.setToolTip(title); }); }; + +function initApplicationMenu() { + if (!trayIcon) { + return; + } + + const contextMenu = Menu.buildFromTemplate([ + { + label: _td('Show/Hide'), + click: toggleWin, + }, + { type: 'separator' }, + { + label: _td('Quit'), + click: function() { + app.quit(); + }, + }, + ]); + + trayIcon.setContextMenu(contextMenu); +} + +exports.initApplicationMenu = initApplicationMenu; diff --git a/src/vectormenu.js b/src/vectormenu.js index 1171d00..223d404 100644 --- a/src/vectormenu.js +++ b/src/vectormenu.js @@ -17,133 +17,137 @@ limitations under the License. const {app, shell, Menu} = require('electron'); const { _td } = require('./language-helper'); -// Menu template from http://electron.atom.io/docs/api/menu/, edited -const template = [ - { - label: _td('Edit'), - accelerator: 'e', - submenu: [ - { role: 'undo' }, - { role: 'redo' }, - { type: 'separator' }, - { role: 'cut' }, - { role: 'copy' }, - { role: 'paste' }, - { role: 'pasteandmatchstyle' }, - { role: 'delete' }, - { role: 'selectall' }, - ], - }, - { - label: _td('View'), - accelerator: 'V', - submenu: [ - { type: 'separator' }, - { role: 'resetzoom' }, - { role: 'zoomin', accelerator: 'CommandOrControl+=' }, - { role: 'zoomout' }, - { type: 'separator' }, - { - label: _td('Preferences'), - accelerator: 'Command+,', // Mac-only accelerator - click() { global.mainWindow.webContents.send('preferences'); }, - }, - { role: 'togglefullscreen' }, - { role: 'toggledevtools' }, - ], - }, - { - label: _td('Window'), - accelerator: 'w', - role: 'window', - submenu: [ - { role: 'minimize' }, - { role: 'close' }, - ], - }, - { - label: _td('Help'), - accelerator: 'h', - role: 'help', - submenu: [ - { - label: _td('Element Help'), - click() { shell.openExternal('https://element.io/help'); }, - }, - ], - }, -]; - -// macOS has specific menu conventions... -if (process.platform === 'darwin') { - template.unshift({ - // first macOS menu is the name of the app - label: app.name, - submenu: [ - { role: 'about' }, - { type: 'separator' }, - { - role: 'services', - submenu: [], - }, - { type: 'separator' }, - { role: 'hide' }, - { role: 'hideothers' }, - { role: 'unhide' }, - { type: 'separator' }, - { role: 'quit' }, - ], - }); - // Edit menu. - // This has a 'speech' section on macOS - template[1].submenu.push( - { type: 'separator' }, +function buildMenuTemplate() { + // Menu template from http://electron.atom.io/docs/api/menu/, edited + const template = [ { - label: _td('Speech'), + label: _td('Edit'), + accelerator: 'e', submenu: [ - { role: 'startspeaking' }, - { role: 'stopspeaking' }, + { role: 'undo' }, + { role: 'redo' }, + { type: 'separator' }, + { role: 'cut' }, + { role: 'copy' }, + { role: 'paste' }, + { role: 'pasteandmatchstyle' }, + { role: 'delete' }, + { role: 'selectall' }, ], - }); - - // Window menu. - // This also has specific functionality on macOS - template[3].submenu = [ - { - label: _td('Close'), - accelerator: 'CmdOrCtrl+W', - role: 'close', }, { - label: _td('Minimize'), - accelerator: 'CmdOrCtrl+M', - role: 'minimize', + label: _td('View'), + accelerator: 'V', + submenu: [ + { type: 'separator' }, + { role: 'resetzoom' }, + { role: 'zoomin', accelerator: 'CommandOrControl+=' }, + { role: 'zoomout' }, + { type: 'separator' }, + { + label: _td('Preferences'), + accelerator: 'Command+,', // Mac-only accelerator + click() { global.mainWindow.webContents.send('preferences'); }, + }, + { role: 'togglefullscreen' }, + { role: 'toggledevtools' }, + ], }, { - label: _td('Zoom'), - role: 'zoom', + label: _td('Window'), + accelerator: 'w', + role: 'window', + submenu: [ + { role: 'minimize' }, + { role: 'close' }, + ], }, { - type: 'separator', - }, - { - label: _td('Bring All to Front'), - role: 'front', + label: _td('Help'), + accelerator: 'h', + role: 'help', + submenu: [ + { + label: _td('Element Help'), + click() { shell.openExternal('https://element.io/help'); }, + }, + ], }, ]; -} else { - template.unshift({ - label: _td('File'), - accelerator: 'f', - submenu: [ - // For some reason, 'about' does not seem to work on windows. - /*{ - role: 'about' - },*/ - { role: 'quit' }, - ], - }); + + // macOS has specific menu conventions... + if (process.platform === 'darwin') { + template.unshift({ + // first macOS menu is the name of the app + label: app.name, + submenu: [ + { role: 'about' }, + { type: 'separator' }, + { + role: 'services', + submenu: [], + }, + { type: 'separator' }, + { role: 'hide' }, + { role: 'hideothers' }, + { role: 'unhide' }, + { type: 'separator' }, + { role: 'quit' }, + ], + }); + // Edit menu. + // This has a 'speech' section on macOS + template[1].submenu.push( + { type: 'separator' }, + { + label: _td('Speech'), + submenu: [ + { role: 'startspeaking' }, + { role: 'stopspeaking' }, + ], + }); + + // Window menu. + // This also has specific functionality on macOS + template[3].submenu = [ + { + label: _td('Close'), + accelerator: 'CmdOrCtrl+W', + role: 'close', + }, + { + label: _td('Minimize'), + accelerator: 'CmdOrCtrl+M', + role: 'minimize', + }, + { + label: _td('Zoom'), + role: 'zoom', + }, + { + type: 'separator', + }, + { + label: _td('Bring All to Front'), + role: 'front', + }, + ]; + } else { + template.unshift({ + label: _td('File'), + accelerator: 'f', + submenu: [ + // For some reason, 'about' does not seem to work on windows. + /*{ + role: 'about' + },*/ + { role: 'quit' }, + ], + }); + } + + return Menu.buildFromTemplate(template); } -module.exports = Menu.buildFromTemplate(template); +module.exports = buildMenuTemplate; diff --git a/yarn.lock b/yarn.lock index 3ad5c5e..d8b4e5b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1597,6 +1597,17 @@ core-util-is@1.0.2, core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= +counterpart@^0.18.6: + version "0.18.6" + resolved "https://registry.yarnpkg.com/counterpart/-/counterpart-0.18.6.tgz#cf6b60d8ef99a4b44b8bf6445fa99b4bd1b2f9dd" + integrity sha512-cAIDAYbC3x8S2DDbvFEJ4TzPtPYXma25/kfAkfmprNLlkPWeX4SdUp1c2xklfphqCU3HnDaivR4R3BrAYf5OMA== + dependencies: + date-names "^0.1.11" + except "^0.1.3" + extend "^3.0.0" + pluralizers "^0.1.7" + sprintf-js "^1.0.3" + crc-32@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.0.tgz#cb2db6e29b88508e32d9dd0ec1693e7b41a18208" @@ -1683,6 +1694,11 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +date-names@^0.1.11: + version "0.1.13" + resolved "https://registry.yarnpkg.com/date-names/-/date-names-0.1.13.tgz#c4358f6f77c8056e2f5ea68fdbb05f0bf1e53bd0" + integrity sha512-IxxoeD9tdx8pXVcmqaRlPvrXIsSrSrIZzfzlOkm9u+hyzKp5Wk/odt9O/gd7Ockzy8n/WHeEpTVJ2bF3mMV4LA== + debounce-fn@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/debounce-fn/-/debounce-fn-4.0.0.tgz#ed76d206d8a50e60de0dd66d494d82835ffe61c7" @@ -2472,6 +2488,13 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +except@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/except/-/except-0.1.3.tgz#98261c91958551536b44482238e9783fb73d292a" + integrity sha1-mCYckZWFUVNrREgiOOl4P7c9KSo= + dependencies: + indexof "0.0.1" + execa@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" @@ -2495,7 +2518,7 @@ exit-on-epipe@~1.0.1: resolved "https://registry.yarnpkg.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692" integrity sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw== -extend@~3.0.2: +extend@^3.0.0, extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -3123,6 +3146,11 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= +indexof@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" + integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10= + infer-owner@^1.0.3, infer-owner@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" @@ -4948,6 +4976,11 @@ plist@^3.0.1: xmlbuilder "^9.0.7" xmldom "^0.5.0" +pluralizers@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/pluralizers/-/pluralizers-0.1.7.tgz#8d38dd0a1b660e739b10ab2eab10b684c9d50142" + integrity sha512-mw6AejUiCaMQ6uPN9ObjJDTnR5AnBSmnHHy3uVTbxrSFSxO5scfwpTs8Dxyb6T2v7GSulhvOq+pm9y+hXUvtOA== + png-to-ico@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/png-to-ico/-/png-to-ico-2.1.1.tgz#35be46f93c1ac8d77025f6f4b60c1fa567c1d47c" @@ -5708,6 +5741,11 @@ split-on-first@^1.0.0: resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== +sprintf-js@^1.0.3: + version "1.1.2" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673" + integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug== + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" From e950303966a5056ef7ec4af72e2bf84608b3d6eb Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 26 Apr 2021 14:13:32 +0100 Subject: [PATCH 03/15] stop using experimental class private methods --- src/language-helper.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/language-helper.js b/src/language-helper.js index 58bb6c9..5fe8de3 100644 --- a/src/language-helper.js +++ b/src/language-helper.js @@ -41,7 +41,7 @@ function _t(text, variables = {}) { class AppLocalization { static STORE_KEY = "locale" - #store = null + store = null constructor({ store, components = [] }) { counterpart.registerTranslations("en", this.fetchTranslationJson("en_EN")); @@ -52,9 +52,9 @@ class AppLocalization { this.localizedComponents = new Set(components); } - this.#store = store; - if (this.#store.has(AppLocalization.STORE_KEY)) { - const locales = this.#store.get(AppLocalization.STORE_KEY); + this.store = store; + if (this.store.has(AppLocalization.STORE_KEY)) { + const locales = this.store.get(AppLocalization.STORE_KEY); this.setAppLocale(locales); } @@ -90,7 +90,7 @@ class AppLocalization { }); counterpart.setLocale(locales); - this.#store.set(AppLocalization.STORE_KEY, locales); + this.store.set(AppLocalization.STORE_KEY, locales); this.resetLocalizedUI(); } From 00ba42186b91594b82d9c06aef272fc3e1d7f7fc Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 26 Apr 2021 14:14:35 +0100 Subject: [PATCH 04/15] Add labels to role-only menu item to have consistent languages in menu --- src/i18n/strings/en_EN.json | 30 ++++++--- src/vectormenu.js | 120 ++++++++++++++++++++++++++++-------- 2 files changed, 119 insertions(+), 31 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a6cd410..b5a97fc 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -5,14 +5,34 @@ "Show/Hide": "Show/Hide", "Quit": "Quit", "Edit": "Edit", + "Undo": "Undo", + "Redo": "Redo", + "Cut": "Cut", + "Copy": "Copy", + "Paste": "Paste", + "Paste and Match Style": "Paste and Match Style", + "Delete": "Delete", + "Select All": "Select All", "View": "View", + "Actual Size": "Actual Size", + "Zoom In": "Zoom In", + "Zoom Out": "Zoom Out", "Preferences": "Preferences", + "Toggle Full Screen": "Toggle Full Screen", + "Toggle Developer Tools": "Toggle Developer Tools", "Window": "Window", + "Minimize": "Minimize", + "Close": "Close", "Help": "Help", "Element Help": "Element Help", + "About": "About", + "Services": "Services", + "Hide": "Hide", + "Hide Others": "Hide Others", + "Unhide": "Unhide", "Speech": "Speech", - "Close": "Close", - "Minimize": "Minimize", + "Start Speaking": "Start Speaking", + "Stop Speaking": "Stop Speaking", "Zoom": "Zoom", "Bring All to Front": "Bring All to Front", "File": "File", @@ -22,9 +42,5 @@ "Save image as...": "Save image as...", "Failed to save image": "Failed to save image", "The image failed to save": "The image failed to save", - "Add to dictionary": "Add to dictionary", - "Cut": "Cut", - "Copy": "Copy", - "Paste": "Paste", - "Select All": "Select All" + "Add to dictionary": "Add to dictionary" } diff --git a/src/vectormenu.js b/src/vectormenu.js index 223d404..368c1ab 100644 --- a/src/vectormenu.js +++ b/src/vectormenu.js @@ -24,15 +24,39 @@ function buildMenuTemplate() { label: _td('Edit'), accelerator: 'e', submenu: [ - { role: 'undo' }, - { role: 'redo' }, + { + role: 'undo', + label: _td('Undo'), + }, + { + role: 'redo', + label: _td('Redo'), + }, { type: 'separator' }, - { role: 'cut' }, - { role: 'copy' }, - { role: 'paste' }, - { role: 'pasteandmatchstyle' }, - { role: 'delete' }, - { role: 'selectall' }, + { + role: 'cut', + label: _td('Cut'), + }, + { + role: 'copy', + label: _td('Copy'), + }, + { + role: 'paste', + label: _td('Paste'), + }, + { + role: 'pasteandmatchstyle', + label: _td('Paste and Match Style'), + }, + { + role: 'delete', + label: _td('Delete'), + }, + { + role: 'selectall', + label: _td('Select All'), + }, ], }, { @@ -40,17 +64,33 @@ function buildMenuTemplate() { accelerator: 'V', submenu: [ { type: 'separator' }, - { role: 'resetzoom' }, - { role: 'zoomin', accelerator: 'CommandOrControl+=' }, - { role: 'zoomout' }, + { + role: 'resetzoom', + label: _td('Actual Size'), + }, + { + role: 'zoomin', + accelerator: 'CommandOrControl+=', + label: _td('Zoom In'), + }, + { + role: 'zoomout', + label: _td('Zoom Out'), + }, { type: 'separator' }, { label: _td('Preferences'), accelerator: 'Command+,', // Mac-only accelerator click() { global.mainWindow.webContents.send('preferences'); }, }, - { role: 'togglefullscreen' }, - { role: 'toggledevtools' }, + { + role: 'togglefullscreen', + label: _td('Toggle Full Screen'), + }, + { + role: 'toggledevtools', + label: _td('Toggle Developer Tools'), + }, ], }, { @@ -58,8 +98,14 @@ function buildMenuTemplate() { accelerator: 'w', role: 'window', submenu: [ - { role: 'minimize' }, - { role: 'close' }, + { + role: 'minimize', + label: _td('Minimize'), + }, + { + role: 'close', + label: _td('Close'), + }, ], }, { @@ -81,18 +127,34 @@ function buildMenuTemplate() { // first macOS menu is the name of the app label: app.name, submenu: [ - { role: 'about' }, + { + role: 'about', + label: _td('About'), + }, { type: 'separator' }, { role: 'services', + label: _td('Services'), submenu: [], }, { type: 'separator' }, - { role: 'hide' }, - { role: 'hideothers' }, - { role: 'unhide' }, + { + role: 'hide', + label: _td('Hide'), + }, + { + role: 'hideothers', + label: _td('Hide Others'), + }, + { + role: 'unhide', + label: _td('Unhide'), + }, { type: 'separator' }, - { role: 'quit' }, + { + role: 'quit', + label: _td('Quit'), + }, ], }); // Edit menu. @@ -102,8 +164,14 @@ function buildMenuTemplate() { { label: _td('Speech'), submenu: [ - { role: 'startspeaking' }, - { role: 'stopspeaking' }, + { + role: 'startspeaking', + label: _td('Start Speaking'), + }, + { + role: 'stopspeaking', + label: _td('Stop Speaking'), + }, ], }); @@ -139,9 +207,13 @@ function buildMenuTemplate() { submenu: [ // For some reason, 'about' does not seem to work on windows. /*{ - role: 'about' + role: 'about', + label: _td('About'), },*/ - { role: 'quit' }, + { + role: 'quit', + label: _td('Quit'), + }, ], }); } From d59384f3e7aba5182361e3f4ecee862f71dd5e96 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 26 Apr 2021 14:19:48 +0100 Subject: [PATCH 05/15] Add internationalisation documentation --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index d035bd7..994c258 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,15 @@ $PROFILE` in which case it becomes `Element-$PROFILE`, or it is using one of the above created by a pre-1.7 install, in which case it will be `Riot` or `Riot-$PROFILE`. +Translations +========================== + +To add a new translation, head to the [translating doc](https://github.com/vector-im/element-web/blob/develop/docs/translating.md). + +For a developer guide, see the [translating dev doc](https://github.com/vector-im/element-web/blob/develop/docs/translating-dev.md). + +[translationsstatus](https://translate.element.io/engage/element-web/?utm_source=widget) + Report bugs & give feedback ========================== From c56aa9100fb7ae8ddec88e599426e36f784564fa Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 26 Apr 2021 14:38:00 +0100 Subject: [PATCH 06/15] appease max line length linting rule --- scripts/gen-i18n.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/gen-i18n.js b/scripts/gen-i18n.js index 2c718a7..0863071 100644 --- a/scripts/gen-i18n.js +++ b/scripts/gen-i18n.js @@ -166,6 +166,7 @@ function getTranslationsJs(file) { if (prop.key.type === 'Literal') { const tag = prop.key.value; // RegExp same as in src/languageHandler.js + // eslint-disable-next-line const regexp = new RegExp(`(<${tag}>(.*?)<\\/${tag}>|<${tag}>|<${tag}\\s*\\/>)`); if (!tKey.match(regexp)) { throw new Error(`No match for ${regexp} in ${tKey}`); From b9510d0a0b219781b41ff2be0a6b7b158fe0532c Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 26 Apr 2021 14:50:18 +0100 Subject: [PATCH 07/15] use _t over _td for consistency with element-web --- src/electron-main.js | 6 +-- src/language-helper.js | 6 +-- src/tray.js | 6 +-- src/vectormenu.js | 76 +++++++++++++++++++------------------- src/webcontents-handler.js | 24 ++++++------ 5 files changed, 59 insertions(+), 59 deletions(-) diff --git a/src/electron-main.js b/src/electron-main.js index 82d0c5b..68c0dd9 100644 --- a/src/electron-main.js +++ b/src/electron-main.js @@ -57,7 +57,7 @@ try { } } -const { _td, AppLocalization } = require('./language-helper'); +const { _t, AppLocalization } = require('./language-helper'); let seshatSupported = false; let Seshat; @@ -271,8 +271,8 @@ const warnBeforeExit = (event, input) => { if (shouldWarnBeforeExit && exitShortcutPressed) { const shouldCancelCloseRequest = dialog.showMessageBoxSync(mainWindow, { type: "question", - buttons: [_td("Cancel"), _td("Close Element")], - message: _td("Are you sure you want to quit?"), + buttons: [_t("Cancel"), _t("Close Element")], + message: _t("Are you sure you want to quit?"), defaultId: 1, cancelId: 0, }) === 0; diff --git a/src/language-helper.js b/src/language-helper.js index 5fe8de3..78c487c 100644 --- a/src/language-helper.js +++ b/src/language-helper.js @@ -2,8 +2,8 @@ const counterpart = require('counterpart'); const DEFAULT_LOCALE = "en"; -function _td(text) { - return _t(text); +function _t(text) { + return text; } function _t(text, variables = {}) { @@ -109,5 +109,5 @@ class AppLocalization { module.exports = { AppLocalization, _t, - _td, + _t, }; diff --git a/src/tray.js b/src/tray.js index 8e03462..7e43025 100644 --- a/src/tray.js +++ b/src/tray.js @@ -19,7 +19,7 @@ const {app, Tray, Menu, nativeImage} = require('electron'); const pngToIco = require('png-to-ico'); const path = require('path'); const fs = require('fs'); -const { _td } = require('./language-helper'); +const { _t } = require('./language-helper'); let trayIcon = null; @@ -98,12 +98,12 @@ function initApplicationMenu() { const contextMenu = Menu.buildFromTemplate([ { - label: _td('Show/Hide'), + label: _t('Show/Hide'), click: toggleWin, }, { type: 'separator' }, { - label: _td('Quit'), + label: _t('Quit'), click: function() { app.quit(); }, diff --git a/src/vectormenu.js b/src/vectormenu.js index 368c1ab..9a44012 100644 --- a/src/vectormenu.js +++ b/src/vectormenu.js @@ -15,106 +15,106 @@ limitations under the License. */ const {app, shell, Menu} = require('electron'); -const { _td } = require('./language-helper'); +const { _t } = require('./language-helper'); function buildMenuTemplate() { // Menu template from http://electron.atom.io/docs/api/menu/, edited const template = [ { - label: _td('Edit'), + label: _t('Edit'), accelerator: 'e', submenu: [ { role: 'undo', - label: _td('Undo'), + label: _t('Undo'), }, { role: 'redo', - label: _td('Redo'), + label: _t('Redo'), }, { type: 'separator' }, { role: 'cut', - label: _td('Cut'), + label: _t('Cut'), }, { role: 'copy', - label: _td('Copy'), + label: _t('Copy'), }, { role: 'paste', - label: _td('Paste'), + label: _t('Paste'), }, { role: 'pasteandmatchstyle', - label: _td('Paste and Match Style'), + label: _t('Paste and Match Style'), }, { role: 'delete', - label: _td('Delete'), + label: _t('Delete'), }, { role: 'selectall', - label: _td('Select All'), + label: _t('Select All'), }, ], }, { - label: _td('View'), + label: _t('View'), accelerator: 'V', submenu: [ { type: 'separator' }, { role: 'resetzoom', - label: _td('Actual Size'), + label: _t('Actual Size'), }, { role: 'zoomin', accelerator: 'CommandOrControl+=', - label: _td('Zoom In'), + label: _t('Zoom In'), }, { role: 'zoomout', - label: _td('Zoom Out'), + label: _t('Zoom Out'), }, { type: 'separator' }, { - label: _td('Preferences'), + label: _t('Preferences'), accelerator: 'Command+,', // Mac-only accelerator click() { global.mainWindow.webContents.send('preferences'); }, }, { role: 'togglefullscreen', - label: _td('Toggle Full Screen'), + label: _t('Toggle Full Screen'), }, { role: 'toggledevtools', - label: _td('Toggle Developer Tools'), + label: _t('Toggle Developer Tools'), }, ], }, { - label: _td('Window'), + label: _t('Window'), accelerator: 'w', role: 'window', submenu: [ { role: 'minimize', - label: _td('Minimize'), + label: _t('Minimize'), }, { role: 'close', - label: _td('Close'), + label: _t('Close'), }, ], }, { - label: _td('Help'), + label: _t('Help'), accelerator: 'h', role: 'help', submenu: [ { - label: _td('Element Help'), + label: _t('Element Help'), click() { shell.openExternal('https://element.io/help'); }, }, ], @@ -129,31 +129,31 @@ function buildMenuTemplate() { submenu: [ { role: 'about', - label: _td('About'), + label: _t('About'), }, { type: 'separator' }, { role: 'services', - label: _td('Services'), + label: _t('Services'), submenu: [], }, { type: 'separator' }, { role: 'hide', - label: _td('Hide'), + label: _t('Hide'), }, { role: 'hideothers', - label: _td('Hide Others'), + label: _t('Hide Others'), }, { role: 'unhide', - label: _td('Unhide'), + label: _t('Unhide'), }, { type: 'separator' }, { role: 'quit', - label: _td('Quit'), + label: _t('Quit'), }, ], }); @@ -162,15 +162,15 @@ function buildMenuTemplate() { template[1].submenu.push( { type: 'separator' }, { - label: _td('Speech'), + label: _t('Speech'), submenu: [ { role: 'startspeaking', - label: _td('Start Speaking'), + label: _t('Start Speaking'), }, { role: 'stopspeaking', - label: _td('Stop Speaking'), + label: _t('Stop Speaking'), }, ], }); @@ -179,40 +179,40 @@ function buildMenuTemplate() { // This also has specific functionality on macOS template[3].submenu = [ { - label: _td('Close'), + label: _t('Close'), accelerator: 'CmdOrCtrl+W', role: 'close', }, { - label: _td('Minimize'), + label: _t('Minimize'), accelerator: 'CmdOrCtrl+M', role: 'minimize', }, { - label: _td('Zoom'), + label: _t('Zoom'), role: 'zoom', }, { type: 'separator', }, { - label: _td('Bring All to Front'), + label: _t('Bring All to Front'), role: 'front', }, ]; } else { template.unshift({ - label: _td('File'), + label: _t('File'), accelerator: 'f', submenu: [ // For some reason, 'about' does not seem to work on windows. /*{ role: 'about', - label: _td('About'), + label: _t('About'), },*/ { role: 'quit', - label: _td('Quit'), + label: _t('Quit'), }, ], }); diff --git a/src/webcontents-handler.js b/src/webcontents-handler.js index a532743..24523e7 100644 --- a/src/webcontents-handler.js +++ b/src/webcontents-handler.js @@ -3,7 +3,7 @@ const url = require('url'); const fs = require('fs'); const request = require('request'); const path = require('path'); -const { _td } = require('./language-helper'); +const { _t } = require('./language-helper'); const MAILTO_PREFIX = "mailto:"; @@ -74,7 +74,7 @@ function onLinkContextMenu(ev, params) { if (params.hasImageContents) { popupMenu.append(new MenuItem({ - label: _td('Copy image'), + label: _t('Copy image'), accelerator: 'c', click() { ev.sender.copyImageAt(params.x, params.y); @@ -87,7 +87,7 @@ function onLinkContextMenu(ev, params) { // Special-case e-mail URLs to strip the `mailto:` like modern browsers do if (url.startsWith(MAILTO_PREFIX)) { popupMenu.append(new MenuItem({ - label: _td('Copy email address'), + label: _t('Copy email address'), accelerator: 'a', click() { clipboard.writeText(url.substr(MAILTO_PREFIX.length)); @@ -95,7 +95,7 @@ function onLinkContextMenu(ev, params) { })); } else { popupMenu.append(new MenuItem({ - label: _td('Copy link address'), + label: _t('Copy link address'), accelerator: 'a', click() { clipboard.writeText(url); @@ -108,7 +108,7 @@ function onLinkContextMenu(ev, params) { // only the renderer can resolve them so don't give the user an option to. if (params.hasImageContents && !url.startsWith('blob:')) { popupMenu.append(new MenuItem({ - label: _td('Save image as...'), + label: _t('Save image as...'), accelerator: 'a', async click() { const targetFileName = params.titleText || "image.png"; @@ -128,8 +128,8 @@ function onLinkContextMenu(ev, params) { console.error(err); dialog.showMessageBox({ type: "error", - title: _td("Failed to save image"), - message: _td("The image failed to save"), + title: _t("Failed to save image"), + message: _t("The image failed to save"), }); } }, @@ -156,7 +156,7 @@ function _CutCopyPasteSelectContextMenus(params) { options.push({ type: 'separator', }, { - label: _td('Add to dictionary'), + label: _t('Add to dictionary'), click: (menuItem, browserWindow) => { browserWindow.webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord); }, @@ -167,17 +167,17 @@ function _CutCopyPasteSelectContextMenus(params) { options.push({ role: 'cut', - label: _td('Cut'), + label: _t('Cut'), accelerator: 't', enabled: params.editFlags.canCut, }, { role: 'copy', - label: _td('Copy'), + label: _t('Copy'), accelerator: 'c', enabled: params.editFlags.canCopy, }, { role: 'paste', - label: _td('Paste'), + label: _t('Paste'), accelerator: 'p', enabled: params.editFlags.canPaste, }, { @@ -185,7 +185,7 @@ function _CutCopyPasteSelectContextMenus(params) { enabled: params.editFlags.canPaste, }, { role: 'selectall', - label: _td("Select All"), + label: _t("Select All"), accelerator: 'a', enabled: params.editFlags.canSelectAll, }); From ae0213b66356091d525a5923893867434d2ef8df Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 26 Apr 2021 16:00:10 +0100 Subject: [PATCH 08/15] Move i18n scripts in its own module --- package.json | 6 +- scripts/gen-i18n.js | 305 -------------------------------------------- yarn.lock | 109 ++++++++++++++++ 3 files changed, 111 insertions(+), 309 deletions(-) delete mode 100644 scripts/gen-i18n.js diff --git a/package.json b/package.json index d651e48..5245a18 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,8 @@ }, "license": "Apache-2.0", "files": [], - "bin": { - "matrix-gen-i18n": "scripts/gen-i18n.js" - }, "scripts": { - "i18n": "matrix-gen-i18n", + "i18n": "gen-18n", "mkdirs": "mkdirp packages deploys", "fetch": "yarn run mkdirs && node scripts/fetch-package.js", "asar-webapp": "asar p webapp webapp.asar", @@ -53,6 +50,7 @@ "find-npm-prefix": "^1.0.2", "fs-extra": "^8.1.0", "glob": "^7.1.6", + "matrix-web-i18n": "github:matrix-org/matrix-web-i18n", "mkdirp": "^1.0.3", "needle": "^2.5.0", "node-pre-gyp": "^0.15.0", diff --git a/scripts/gen-i18n.js b/scripts/gen-i18n.js deleted file mode 100644 index 0863071..0000000 --- a/scripts/gen-i18n.js +++ /dev/null @@ -1,305 +0,0 @@ -#!/usr/bin/env node - -/* -Copyright 2017 New Vector 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. -*/ - -/** - * Regenerates the translations en_EN file by walking the source tree and - * parsing each file with the appropriate parser. Emits a JSON file with the - * translatable strings mapped to themselves in the order they appeared - * in the files and grouped by the file they appeared in. - * - * Usage: node scripts/gen-i18n.js - */ -const fs = require('fs'); -const path = require('path'); - -const walk = require('walk'); - -const parser = require("@babel/parser"); -const traverse = require("@babel/traverse"); - -const TRANSLATIONS_FUNCS = ['_t', '_td']; - -const INPUT_TRANSLATIONS_FILE = 'src/i18n/strings/en_EN.json'; -const OUTPUT_FILE = 'src/i18n/strings/en_EN.json'; - -// NB. The sync version of walk is broken for single files so we walk -// all of res rather than just res/home.html. -// https://git.daplie.com/Daplie/node-walk/merge_requests/1 fixes it, -// or if we get bored waiting for it to be merged, we could switch -// to a project that's actively maintained. -const SEARCH_PATHS = ['src', 'res']; - -function getObjectValue(obj, key) { - for (const prop of obj.properties) { - if (prop.key.type === 'Identifier' && prop.key.name === key) { - return prop.value; - } - } - return null; -} - -function getTKey(arg) { - if (arg.type === 'Literal' || arg.type === "StringLiteral") { - return arg.value; - } else if (arg.type === 'BinaryExpression' && arg.operator === '+') { - return getTKey(arg.left) + getTKey(arg.right); - } else if (arg.type === 'TemplateLiteral') { - return arg.quasis.map((q) => { - return q.value.raw; - }).join(''); - } - return null; -} - -function getFormatStrings(str) { - // Match anything that starts with % - // We could make a regex that matched the full placeholder, but this - // would just not match invalid placeholders and so wouldn't help us - // detect the invalid ones. - // Also note that for simplicity, this just matches a % character and then - // anything up to the next % character (or a single %, or end of string). - const formatStringRe = /%([^%]+|%|$)/g; - const formatStrings = new Set(); - - let match; - while ( (match = formatStringRe.exec(str)) !== null ) { - const placeholder = match[1]; // Minus the leading '%' - if (placeholder === '%') continue; // Literal % is %% - - const placeholderMatch = placeholder.match(/^\((.*?)\)(.)/); - if (placeholderMatch === null) { - throw new Error("Invalid format specifier: '"+match[0]+"'"); - } - if (placeholderMatch.length < 3) { - throw new Error("Malformed format specifier"); - } - const placeholderName = placeholderMatch[1]; - const placeholderFormat = placeholderMatch[2]; - - if (placeholderFormat !== 's') { - throw new Error(`'${placeholderFormat}' used as format character: you probably meant 's'`); - } - - formatStrings.add(placeholderName); - } - - return formatStrings; -} - -function getTranslationsJs(file) { - const contents = fs.readFileSync(file, { encoding: 'utf8' }); - - const trs = new Set(); - - try { - const plugins = [ - // https://babeljs.io/docs/en/babel-parser#plugins - "classProperties", - "objectRestSpread", - "throwExpressions", - "exportDefaultFrom", - "decorators-legacy", - ]; - - if (file.endsWith(".js") || file.endsWith(".jsx")) { - // all JS is assumed to be flow or react - plugins.push("flow", "jsx"); - } else if (file.endsWith(".ts")) { - // TS can't use JSX unless it's a TSX file (otherwise angle casts fail) - plugins.push("typescript"); - } else if (file.endsWith(".tsx")) { - // When the file is a TSX file though, enable JSX parsing - plugins.push("typescript", "jsx"); - } - - const babelParsed = parser.parse(contents, { - allowImportExportEverywhere: true, - errorRecovery: true, - sourceFilename: file, - tokens: true, - plugins, - }); - traverse.default(babelParsed, { - enter: (p) => { - const node = p.node; - if (p.isCallExpression() && node.callee && TRANSLATIONS_FUNCS.includes(node.callee.name)) { - const tKey = getTKey(node.arguments[0]); - - // This happens whenever we call _t with non-literals (ie. whenever we've - // had to use a _td to compensate) so is expected. - if (tKey === null) return; - - // check the format string against the args - // We only check _t: _td has no args - if (node.callee.name === '_t') { - try { - const placeholders = getFormatStrings(tKey); - for (const placeholder of placeholders) { - if (node.arguments.length < 2) { - throw new Error(`Placeholder found ('${placeholder}') but no substitutions given`); - } - const value = getObjectValue(node.arguments[1], placeholder); - if (value === null) { - throw new Error(`No value found for placeholder '${placeholder}'`); - } - } - - // Validate tag replacements - if (node.arguments.length > 2) { - const tagMap = node.arguments[2]; - for (const prop of tagMap.properties || []) { - if (prop.key.type === 'Literal') { - const tag = prop.key.value; - // RegExp same as in src/languageHandler.js - // eslint-disable-next-line - const regexp = new RegExp(`(<${tag}>(.*?)<\\/${tag}>|<${tag}>|<${tag}\\s*\\/>)`); - if (!tKey.match(regexp)) { - throw new Error(`No match for ${regexp} in ${tKey}`); - } - } - } - } - } catch (e) { - console.log(); - console.error(`ERROR: ${file}:${node.loc.start.line} ${tKey}`); - console.error(e); - process.exit(1); - } - } - - let isPlural = false; - if (node.arguments.length > 1 && node.arguments[1].type === 'ObjectExpression') { - const countVal = getObjectValue(node.arguments[1], 'count'); - if (countVal) { - isPlural = true; - } - } - - if (isPlural) { - trs.add(tKey + "|other"); - const plurals = enPlurals[tKey]; - if (plurals) { - for (const pluralType of Object.keys(plurals)) { - trs.add(tKey + "|" + pluralType); - } - } - } else { - trs.add(tKey); - } - } - }, - }); - } catch (e) { - console.error(e); - process.exit(1); - } - - return trs; -} - -function getTranslationsOther(file) { - const contents = fs.readFileSync(file, { encoding: 'utf8' }); - - const trs = new Set(); - - // Taken from element-web src/components/structures/HomePage.js - const translationsRegex = /_t\(['"]([\s\S]*?)['"]\)/mg; - let matches; - while (matches = translationsRegex.exec(contents)) { - trs.add(matches[1]); - } - return trs; -} - -// gather en_EN plural strings from the input translations file: -// the en_EN strings are all in the source with the exception of -// pluralised strings, which we need to pull in from elsewhere. -const inputTranslationsRaw = JSON.parse(fs.readFileSync(INPUT_TRANSLATIONS_FILE, { encoding: 'utf8' })); -const enPlurals = {}; - -for (const key of Object.keys(inputTranslationsRaw)) { - const parts = key.split("|"); - if (parts.length > 1) { - const plurals = enPlurals[parts[0]] || {}; - plurals[parts[1]] = inputTranslationsRaw[key]; - enPlurals[parts[0]] = plurals; - } -} - -const translatables = new Set(); - -const walkOpts = { - listeners: { - names: function(root, nodeNamesArray) { - // Sort the names case insensitively and alphabetically to - // maintain some sense of order between the different strings. - nodeNamesArray.sort((a, b) => { - a = a.toLowerCase(); - b = b.toLowerCase(); - if (a > b) return 1; - if (a < b) return -1; - return 0; - }); - }, - file: function(root, fileStats, next) { - const fullPath = path.join(root, fileStats.name); - - let trs; - if (fileStats.name.endsWith('.js') || fileStats.name.endsWith('.ts') || fileStats.name.endsWith('.tsx')) { - trs = getTranslationsJs(fullPath); - } else if (fileStats.name.endsWith('.html')) { - trs = getTranslationsOther(fullPath); - } else { - return; - } - console.log(`${fullPath} (${trs.size} strings)`); - for (const tr of trs.values()) { - // Convert DOS line endings to unix - translatables.add(tr.replace(/\r\n/g, "\n")); - } - }, - }, -}; - -for (const path of SEARCH_PATHS) { - if (fs.existsSync(path)) { - walk.walkSync(path, walkOpts); - } -} - -const trObj = {}; -for (const tr of translatables) { - if (tr.includes("|")) { - if (inputTranslationsRaw[tr]) { - trObj[tr] = inputTranslationsRaw[tr]; - } else { - trObj[tr] = tr.split("|")[0]; - } - } else { - trObj[tr] = tr; - } -} - -fs.writeFileSync( - OUTPUT_FILE, - JSON.stringify(trObj, translatables.values(), 4) + "\n", -); - -console.log(); -console.log(`Wrote ${translatables.size} strings to ${OUTPUT_FILE}`); - diff --git a/yarn.lock b/yarn.lock index d8b4e5b..7fb03ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26,6 +26,13 @@ dependencies: "@babel/highlight" "^7.10.4" +"@babel/code-frame@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658" + integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g== + dependencies: + "@babel/highlight" "^7.12.13" + "@babel/generator@^7.10.5": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.10.5.tgz#1b903554bc8c583ee8d25f1e8969732e6b829a69" @@ -35,6 +42,15 @@ jsesc "^2.5.1" source-map "^0.5.0" +"@babel/generator@^7.13.16": + version "7.13.16" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.13.16.tgz#0befc287031a201d84cdfc173b46b320ae472d14" + integrity sha512-grBBR75UnKOcUWMp8WoDxNsWCFl//XCK6HWTrBQKTr5SV9f5g0pNOjdyzi/DTBv12S9GnYPInIXQBTky7OXEMg== + dependencies: + "@babel/types" "^7.13.16" + jsesc "^2.5.1" + source-map "^0.5.0" + "@babel/helper-function-name@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a" @@ -44,6 +60,15 @@ "@babel/template" "^7.10.4" "@babel/types" "^7.10.4" +"@babel/helper-function-name@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz#93ad656db3c3c2232559fd7b2c3dbdcbe0eb377a" + integrity sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA== + dependencies: + "@babel/helper-get-function-arity" "^7.12.13" + "@babel/template" "^7.12.13" + "@babel/types" "^7.12.13" + "@babel/helper-get-function-arity@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" @@ -51,6 +76,13 @@ dependencies: "@babel/types" "^7.10.4" +"@babel/helper-get-function-arity@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz#bc63451d403a3b3082b97e1d8b3fe5bd4091e583" + integrity sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg== + dependencies: + "@babel/types" "^7.12.13" + "@babel/helper-split-export-declaration@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.4.tgz#2c70576eaa3b5609b24cb99db2888cc3fc4251d1" @@ -58,11 +90,23 @@ dependencies: "@babel/types" "^7.10.4" +"@babel/helper-split-export-declaration@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz#e9430be00baf3e88b0e13e6f9d4eaf2136372b05" + integrity sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg== + dependencies: + "@babel/types" "^7.12.13" + "@babel/helper-validator-identifier@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== +"@babel/helper-validator-identifier@^7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" + integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== + "@babel/highlight@^7.0.0": version "7.5.0" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.5.0.tgz#56d11312bd9248fa619591d02472be6e8cb32540" @@ -81,11 +125,25 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.12.13": + version "7.13.10" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.13.10.tgz#a8b2a66148f5b27d666b15d81774347a731d52d1" + integrity sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg== + dependencies: + "@babel/helper-validator-identifier" "^7.12.11" + chalk "^2.0.0" + js-tokens "^4.0.0" + "@babel/parser@^7.10.4", "@babel/parser@^7.10.5", "@babel/parser@^7.7.0": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.5.tgz#e7c6bf5a7deff957cec9f04b551e2762909d826b" integrity sha512-wfryxy4bE1UivvQKSQDU4/X6dr+i8bctjUjj8Zyt3DQy7NtPizJXT8M52nqpNKL+nq2PW8lxk4ZqLj0fD4B4hQ== +"@babel/parser@^7.12.13", "@babel/parser@^7.13.16": + version "7.13.16" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.16.tgz#0f18179b0448e6939b1f3f5c4c355a3a9bcdfd37" + integrity sha512-6bAg36mCwuqLO0hbR+z7PHuqWiCeP7Dzg73OpQwsAB1Eb8HnGEz5xYBzCfbu+YjoaJsJs+qheDxVAuqbt3ILEw== + "@babel/runtime@^7.7.2": version "7.11.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" @@ -102,6 +160,29 @@ "@babel/parser" "^7.10.4" "@babel/types" "^7.10.4" +"@babel/template@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.13.tgz#530265be8a2589dbb37523844c5bcb55947fb327" + integrity sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA== + dependencies: + "@babel/code-frame" "^7.12.13" + "@babel/parser" "^7.12.13" + "@babel/types" "^7.12.13" + +"@babel/traverse@^7.13.17": + version "7.13.17" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.13.17.tgz#c85415e0c7d50ac053d758baec98b28b2ecfeea3" + integrity sha512-BMnZn0R+X6ayqm3C3To7o1j7Q020gWdqdyP50KEoVqaCO2c/Im7sYZSmVgvefp8TTMQ+9CtwuBp0Z1CZ8V3Pvg== + dependencies: + "@babel/code-frame" "^7.12.13" + "@babel/generator" "^7.13.16" + "@babel/helper-function-name" "^7.12.13" + "@babel/helper-split-export-declaration" "^7.12.13" + "@babel/parser" "^7.13.16" + "@babel/types" "^7.13.17" + debug "^4.1.0" + globals "^11.1.0" + "@babel/traverse@^7.7.0": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.10.5.tgz#77ce464f5b258be265af618d8fddf0536f20b564" @@ -126,6 +207,14 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" +"@babel/types@^7.12.13", "@babel/types@^7.13.16", "@babel/types@^7.13.17": + version "7.13.17" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.17.tgz#48010a115c9fba7588b4437dd68c9469012b38b4" + integrity sha512-RawydLgxbOPDlTLJNtoIypwdmAy//uQIzlKt2+iBiJaRlVuI6QLUxVAyWGNfOzp8Yu4L4lLIacoCyTNtpb4wiA== + dependencies: + "@babel/helper-validator-identifier" "^7.12.11" + to-fast-properties "^2.0.0" + "@develar/schema-utils@~2.6.5": version "2.6.5" resolved "https://registry.yarnpkg.com/@develar/schema-utils/-/schema-utils-2.6.5.tgz#3ece22c5838402419a6e0425f85742b961d9b6c6" @@ -2634,6 +2723,11 @@ flush-write-stream@^1.0.0: inherits "^2.0.3" readable-stream "^2.3.6" +foreachasync@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/foreachasync/-/foreachasync-3.0.0.tgz#5502987dc8714be3392097f32e0071c9dee07cf6" + integrity sha1-VQKYfchxS+M5IJfzLgBxyd7gfPY= + forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -4003,6 +4097,14 @@ make-fetch-happen@^5.0.0: socks-proxy-agent "^4.0.0" ssri "^6.0.0" +"matrix-web-i18n@github:matrix-org/matrix-web-i18n": + version "1.0.1" + resolved "https://codeload.github.com/matrix-org/matrix-web-i18n/tar.gz/6df80d0197711e9f9733e9e3e2ef1d38a2ab8949" + dependencies: + "@babel/parser" "^7.13.16" + "@babel/traverse" "^7.13.17" + walk "^2.3.14" + meant@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/meant/-/meant-1.0.3.tgz#67769af9de1d158773e928ae82c456114903554c" @@ -6440,6 +6542,13 @@ version-range@^1.0.0: dependencies: version-compare "^1.0.0" +walk@^2.3.14: + version "2.3.14" + resolved "https://registry.yarnpkg.com/walk/-/walk-2.3.14.tgz#60ec8631cfd23276ae1e7363ce11d626452e1ef3" + integrity sha512-5skcWAUmySj6hkBdH6B6+3ddMjVQYH5Qy9QGbPmN8kVmLteXk+yVXg+yfk1nbX30EYakahLrr8iPcCxJQSCBeg== + dependencies: + foreachasync "^3.0.0" + wcwidth@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" From de1609936054b8aff526085f6e989574bcbd593b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 26 Apr 2021 09:11:30 -0600 Subject: [PATCH 09/15] Add required weblate basefile --- src/i18n/strings/basefile.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/i18n/strings/basefile.json diff --git a/src/i18n/strings/basefile.json b/src/i18n/strings/basefile.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/src/i18n/strings/basefile.json @@ -0,0 +1 @@ +{} \ No newline at end of file From 72483a60cbde423052478f74bec2f6f1d64a8a7c Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 26 Apr 2021 16:22:29 +0100 Subject: [PATCH 10/15] Add diff-i18n script for CI --- package.json | 4 +++- yarn.lock | 15 ++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 5245a18..a66f965 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,9 @@ "license": "Apache-2.0", "files": [], "scripts": { - "i18n": "gen-18n", + "i18n": "gen-i18n", + "prunei18n": "prune-i18n", + "diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && gen-i18n && compare-i18n-files src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json", "mkdirs": "mkdirp packages deploys", "fetch": "yarn run mkdirs && node scripts/fetch-package.js", "asar-webapp": "asar p webapp webapp.asar", diff --git a/yarn.lock b/yarn.lock index 7fb03ee..2c03898 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1816,14 +1816,14 @@ debug@^3.1.0, debug@^3.2.6: dependencies: ms "^2.1.1" -debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: +debug@^4.0.1, debug@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== dependencies: ms "^2.1.1" -debug@^4.3.1: +debug@^4.1.0, debug@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== @@ -4098,8 +4098,8 @@ make-fetch-happen@^5.0.0: ssri "^6.0.0" "matrix-web-i18n@github:matrix-org/matrix-web-i18n": - version "1.0.1" - resolved "https://codeload.github.com/matrix-org/matrix-web-i18n/tar.gz/6df80d0197711e9f9733e9e3e2ef1d38a2ab8949" + version "1.1.1" + resolved "https://codeload.github.com/matrix-org/matrix-web-i18n/tar.gz/68ea0c57b6c74c40df6419eb5ac0fa8945ff8a75" dependencies: "@babel/parser" "^7.13.16" "@babel/traverse" "^7.13.17" @@ -4270,11 +4270,16 @@ ms@2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= -ms@2.1.2, ms@^2.0.0, ms@^2.1.1: +ms@2.1.2, ms@^2.0.0: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + mute-stream@0.0.8, mute-stream@~0.0.4: version "0.0.8" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" From 20ce3d10a3ee91606fb3bfc27a738d04b4e00c6e Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 26 Apr 2021 16:58:22 +0100 Subject: [PATCH 11/15] add missing copyright header --- src/language-helper.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/language-helper.js b/src/language-helper.js index 78c487c..4964693 100644 --- a/src/language-helper.js +++ b/src/language-helper.js @@ -1,3 +1,19 @@ +/* +Copyright 2021 New Vector 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 counterpart = require('counterpart'); const DEFAULT_LOCALE = "en"; From 8f4c827089602e4f5b72c1c151c21404e7787cf4 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 26 Apr 2021 16:59:24 +0100 Subject: [PATCH 12/15] change translate widget project ID --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 994c258..bbd4b96 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ To add a new translation, head to the [translating doc](https://github.com/vecto For a developer guide, see the [translating dev doc](https://github.com/vector-im/element-web/blob/develop/docs/translating-dev.md). -[translationsstatus](https://translate.element.io/engage/element-web/?utm_source=widget) +[translationsstatus](https://translate.element.io/engage/element-desktop/?utm_source=widget) Report bugs & give feedback ========================== From 8ae641bc5977426d2aaed9ec72bf41cacb582814 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 26 Apr 2021 17:12:13 +0100 Subject: [PATCH 13/15] Set appropriate appMenu submenu role --- src/vectormenu.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vectormenu.js b/src/vectormenu.js index 9a44012..8f1fb63 100644 --- a/src/vectormenu.js +++ b/src/vectormenu.js @@ -125,6 +125,7 @@ function buildMenuTemplate() { if (process.platform === 'darwin') { template.unshift({ // first macOS menu is the name of the app + role: 'appMenu', label: app.name, submenu: [ { From 46b1015478854cacdd5b09d97cf3728e556c2a7c Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 26 Apr 2021 17:14:55 +0100 Subject: [PATCH 14/15] Fix typo in _t function declaration --- src/language-helper.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/language-helper.js b/src/language-helper.js index 4964693..baf6c46 100644 --- a/src/language-helper.js +++ b/src/language-helper.js @@ -18,7 +18,7 @@ const counterpart = require('counterpart'); const DEFAULT_LOCALE = "en"; -function _t(text) { +function _td(text) { return text; } @@ -125,5 +125,5 @@ class AppLocalization { module.exports = { AppLocalization, _t, - _t, + _td, }; From e64e6e642c1594b560ac6714396f8aa7501547d7 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 27 Apr 2021 15:10:20 +0100 Subject: [PATCH 15/15] Upgrade matrix-web-i18n to use matrix prefixed binaries --- package.json | 6 +++--- yarn.lock | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a66f965..1ca16bf 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,9 @@ "license": "Apache-2.0", "files": [], "scripts": { - "i18n": "gen-i18n", - "prunei18n": "prune-i18n", - "diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && gen-i18n && compare-i18n-files src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json", + "i18n": "matrix-gen-i18n", + "prunei18n": "matrix-prune-i18n", + "diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && matrix-gen-i18n && matrix-compare-i18n-files src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json", "mkdirs": "mkdirp packages deploys", "fetch": "yarn run mkdirs && node scripts/fetch-package.js", "asar-webapp": "asar p webapp webapp.asar", diff --git a/yarn.lock b/yarn.lock index 2c03898..1b9aedb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4098,8 +4098,8 @@ make-fetch-happen@^5.0.0: ssri "^6.0.0" "matrix-web-i18n@github:matrix-org/matrix-web-i18n": - version "1.1.1" - resolved "https://codeload.github.com/matrix-org/matrix-web-i18n/tar.gz/68ea0c57b6c74c40df6419eb5ac0fa8945ff8a75" + version "1.1.2" + resolved "https://codeload.github.com/matrix-org/matrix-web-i18n/tar.gz/63f9119bc0bc304e83d4e8e22364caa7850e7671" dependencies: "@babel/parser" "^7.13.16" "@babel/traverse" "^7.13.17"