-
{label}
-
+
+
+ {label}
+
+
{accountSub} ({account.displayProvider()})
diff --git a/app/internal_packages/thread-list/lib/thread-list-columns.jsx b/app/internal_packages/thread-list/lib/thread-list-columns.jsx
index 1e982debd..2c7943024 100644
--- a/app/internal_packages/thread-list/lib/thread-list-columns.jsx
+++ b/app/internal_packages/thread-list/lib/thread-list-columns.jsx
@@ -120,8 +120,12 @@ const c3 = new ListTabular.Column({
return (
- {subject(thread.subject)}
- {getSnippet(thread)}
+
+ {subject(thread.subject)}
+
+
+ {getSnippet(thread)}
+
{attachment}
);
@@ -221,9 +225,13 @@ const cNarrow = new ListTabular.Column({
matching={{ role: 'ThreadListTimestamp' }}
/>
-
{subject(thread.subject)}
+
+ {subject(thread.subject)}
+
-
{getSnippet(thread)}
+
+ {getSnippet(thread)}
+
diff --git a/app/internal_packages/thread-list/lib/thread-list-participants.jsx b/app/internal_packages/thread-list/lib/thread-list-participants.jsx
index c19fcbb0b..022088848 100644
--- a/app/internal_packages/thread-list/lib/thread-list-participants.jsx
+++ b/app/internal_packages/thread-list/lib/thread-list-participants.jsx
@@ -14,7 +14,11 @@ class ThreadListParticipants extends React.Component {
render() {
const items = this.getTokens();
- return
{this.renderSpans(items)}
;
+ return (
+
+ {this.renderSpans(items)}
+
+ );
}
renderSpans(items) {
diff --git a/app/package-lock.json b/app/package-lock.json
index 12585901f..d01d1d810 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -943,6 +943,19 @@
"void-elements": "2.0.1"
}
},
+ "color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
+ },
"colors": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/colors/-/colors-0.5.1.tgz",
@@ -1620,6 +1633,27 @@
}
}
},
+ "findup": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/findup/-/findup-0.1.5.tgz",
+ "integrity": "sha1-itkpozk7rGJ5V6fl3kYjsGsOLOs=",
+ "requires": {
+ "colors": "0.6.2",
+ "commander": "2.1.0"
+ },
+ "dependencies": {
+ "colors": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz",
+ "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w="
+ },
+ "commander": {
+ "version": "2.1.0",
+ "resolved": "http://registry.npmjs.org/commander/-/commander-2.1.0.tgz",
+ "integrity": "sha1-0SG7roYNmZKj1Re6lvVliOR8Z4E="
+ }
+ }
+ },
"foreach": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz",
@@ -1843,6 +1877,11 @@
"ansi-regex": "2.1.1"
}
},
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
+ },
"has-symbol-support-x": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.1.tgz",
@@ -3140,6 +3179,49 @@
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw="
},
+ "postcss": {
+ "version": "6.0.23",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz",
+ "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==",
+ "requires": {
+ "chalk": "2.4.1",
+ "source-map": "0.6.1",
+ "supports-color": "5.5.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "requires": {
+ "color-convert": "1.9.3"
+ }
+ },
+ "chalk": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
+ "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
+ "requires": {
+ "ansi-styles": "3.2.1",
+ "escape-string-regexp": "1.0.5",
+ "supports-color": "5.5.0"
+ }
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "requires": {
+ "has-flag": "3.0.0"
+ }
+ }
+ }
+ },
"prebuild-install": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-2.5.0.tgz",
@@ -3594,6 +3676,46 @@
"nearley": "2.11.0"
}
},
+ "rtlcss": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-2.4.0.tgz",
+ "integrity": "sha512-hdjFhZ5FCI0ABOfyXOMOhBtwPWtANLCG7rOiOcRf+yi5eDdxmDjqBruWouEnwVdzfh/TWF6NNncIEsigOCFZOA==",
+ "requires": {
+ "chalk": "2.4.1",
+ "findup": "0.1.5",
+ "mkdirp": "0.5.1",
+ "postcss": "6.0.23",
+ "strip-json-comments": "2.0.1"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "requires": {
+ "color-convert": "1.9.3"
+ }
+ },
+ "chalk": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
+ "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
+ "requires": {
+ "ansi-styles": "3.2.1",
+ "escape-string-regexp": "1.0.5",
+ "supports-color": "5.5.0"
+ }
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "requires": {
+ "has-flag": "3.0.0"
+ }
+ }
+ }
+ },
"runas": {
"version": "github:getflywheel/node-runas#ca4f0714a926319f53df4851b007f2e4fd5381e5",
"requires": {
diff --git a/app/package.json b/app/package.json
index 1a6e80b8d..55d169335 100644
--- a/app/package.json
+++ b/app/package.json
@@ -53,6 +53,7 @@
"reflux": "0.1.13",
"rimraf": "2.5.2",
"runas": "getflywheel/node-runas#ca4f0714",
+ "rtlcss": "2.4.0",
"rx-lite": "4.0.8",
"slate": "bengotow/slate#77f6337",
"slate-auto-replace": "0.9.0",
diff --git a/app/src/app-env.es6 b/app/src/app-env.es6
index 657b1150e..5a56cfc71 100644
--- a/app/src/app-env.es6
+++ b/app/src/app-env.es6
@@ -5,7 +5,7 @@ import path from 'path';
import { ipcRenderer, remote } from 'electron';
import { Emitter } from 'event-kit';
import { mapSourcePosition } from 'source-map-support';
-import { localized } from './intl';
+import { localized, isRTL } from './intl';
import { APIError } from './flux/errors';
import WindowEventHandler from './window-event-handler';
@@ -727,6 +727,9 @@ export default class AppEnvConstructor {
this.item.setAttribute('id', 'sheet-container');
this.item.setAttribute('class', 'sheet-container');
this.item.setAttribute('tabIndex', '-1');
+ if (isRTL) {
+ this.item.setAttribute('dir', 'rtl');
+ }
const React = require('react');
const ReactDOM = require('react-dom');
diff --git a/app/src/components/scroll-region.jsx b/app/src/components/scroll-region.jsx
index 90111a505..e632f24e7 100644
--- a/app/src/components/scroll-region.jsx
+++ b/app/src/components/scroll-region.jsx
@@ -1,5 +1,5 @@
const _ = require('underscore');
-const { React, ReactDOM, PropTypes, Utils } = require('mailspring-exports');
+const { React, ReactDOM, PropTypes, Utils, isRTL } = require('mailspring-exports');
const classNames = require('classnames');
const ScrollbarTicks = require('./scrollbar-ticks').default;
@@ -150,7 +150,7 @@ class Scrollbar extends React.Component {
position: 'absolute',
top: 0,
bottom: 0,
- right: 0,
+ [isRTL ? 'left' : 'right']: 0,
zIndex: 2,
visibility:
this.state.totalHeight !== 0 && this.state.totalHeight === this.state.viewportHeight
diff --git a/app/src/global/mailspring-exports.es6 b/app/src/global/mailspring-exports.es6
index daf6e0e8d..4354bd64f 100644
--- a/app/src/global/mailspring-exports.es6
+++ b/app/src/global/mailspring-exports.es6
@@ -49,6 +49,7 @@ const lazyLoadAndRegisterTask = (klassName, path) => {
lazyLoadWithGetter(`localized`, () => require('../intl').localized);
lazyLoadWithGetter(`localizedReactFragment`, () => require('../intl').localizedReactFragment);
+lazyLoadWithGetter(`isRTL`, () => require('../intl').isRTL);
// Actions
lazyLoad(`Actions`, 'flux/actions');
diff --git a/app/src/intl.es6 b/app/src/intl.es6
index a850796f2..584d59fbd 100644
--- a/app/src/intl.es6
+++ b/app/src/intl.es6
@@ -2,16 +2,41 @@ const fs = require('fs');
const path = require('path');
const React = process.type === 'renderer' ? require('react') : null;
+const RTL_LANGS = [
+ 'ae' /* Avestan */,
+ 'ar' /* 'العربية', Arabic */,
+ 'arc' /* Aramaic */,
+ 'bcc' /* 'بلوچی مکرانی', Southern Balochi */,
+ 'bqi' /* 'بختياري', Bakthiari */,
+ 'ckb' /* 'Soranî / کوردی', Sorani */,
+ 'dv' /* Dhivehi */,
+ 'fa' /* 'فارسی', Persian */,
+ 'glk' /* 'گیلکی', Gilaki */,
+ 'he' /* 'עברית', Hebrew */,
+ 'ku' /* 'Kurdî / كوردی', Kurdish */,
+ 'mzn' /* 'مازِرونی', Mazanderani */,
+ 'nqo' /* N'Ko */,
+ 'pnb' /* 'پنجابی', Western Punjabi */,
+ 'ps' /* 'پښتو', Pashto, */,
+ 'sd' /* 'سنڌي', Sindhi */,
+ 'ug' /* 'Uyghurche / ئۇيغۇرچە', Uyghur */,
+ 'ur' /* 'اردو', Urdu */,
+ 'yi' /* 'ייִדיש', Yiddish */,
+];
+
const locale =
process.type === 'renderer' ? window.navigator.language : require('electron').app.getLocale();
const lang = locale.split('-')[0] || 'en';
const langsDir = path.join(__dirname, '..', 'lang');
+export let isRTL = false;
+
let localizations = {};
// Load localizations for the base language (eg: `zh`)
if (fs.existsSync(path.join(langsDir, `${lang}.json`))) {
localizations = require(path.join(langsDir, `${lang}.json`));
+ isRTL = RTL_LANGS.includes(lang);
}
// Load localizations for the full locale if present (eg: `zh-CN`)
diff --git a/app/src/less-compile-cache.es6 b/app/src/less-compile-cache.es6
index 779e352ff..5997195a5 100644
--- a/app/src/less-compile-cache.es6
+++ b/app/src/less-compile-cache.es6
@@ -1,6 +1,9 @@
import _ from 'underscore';
import path from 'path';
import LessCache from 'less-cache';
+import rtlcss from 'rtlcss';
+
+import { isRTL } from './intl';
// {LessCache} wrapper used by {ThemeManager} to read stylesheets.
export default class LessCompileCache {
@@ -16,6 +19,16 @@ export default class LessCompileCache {
importPaths: importPaths.concat(this.lessSearchPaths),
resourcePath: resourcePath,
});
+
+ // swizzle the LessCache parseLess method to perform rtl conversion
+ // on the final CSS, and THEN save the cached result / return the CSS
+ if (isRTL) {
+ const original = this.cache.parseLess;
+ this.cache.parseLess = (filePath, contents) => {
+ const { imports, css } = original.call(this.cache, filePath, contents);
+ return { imports, css: rtlcss.process(css) };
+ };
+ }
}
// Setting the import paths is a VERY expensive operation (200ms +)
diff --git a/app/src/sheet-toolbar.jsx b/app/src/sheet-toolbar.jsx
index 687071200..75b4e68eb 100644
--- a/app/src/sheet-toolbar.jsx
+++ b/app/src/sheet-toolbar.jsx
@@ -4,7 +4,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import { remote } from 'electron';
-import { localized, Actions, ComponentRegistry, WorkspaceStore } from 'mailspring-exports';
+import { localized, isRTL, Actions, ComponentRegistry, WorkspaceStore } from 'mailspring-exports';
import Flexbox from './components/flexbox';
import RetinaImg from './components/retina-img';
@@ -89,7 +89,11 @@ class ToolbarBack extends React.Component {
}
return (
);
@@ -180,12 +184,19 @@ class ToolbarMenuControl extends React.Component {
}
}
+// macOS and other platforms do not flip the corner that the window controls
+// appear in when displaying the UI RTL, but Chrome flips our whole UI. We need
+// to intentionally undo the flip.
ComponentRegistry.register(ToolbarWindowControls, {
- location: WorkspaceStore.Sheet.Global.Toolbar.Left,
+ location: isRTL
+ ? WorkspaceStore.Sheet.Global.Toolbar.Right
+ : WorkspaceStore.Sheet.Global.Toolbar.Left,
});
ComponentRegistry.register(ToolbarMenuControl, {
- location: WorkspaceStore.Sheet.Global.Toolbar.Right,
+ location: isRTL
+ ? WorkspaceStore.Sheet.Global.Toolbar.Left
+ : WorkspaceStore.Sheet.Global.Toolbar.Right,
});
export default class Toolbar extends React.Component {
diff --git a/app/static/components/outline-view.less b/app/static/components/outline-view.less
index 3ba837bf1..169254d9d 100644
--- a/app/static/components/outline-view.less
+++ b/app/static/components/outline-view.less
@@ -1,5 +1,5 @@
-@import "ui-variables";
-@import "ui-mixins";
+@import 'ui-variables';
+@import 'ui-mixins';
@count-color: fadeout(@text-color-subtle, 40%);
@@ -22,8 +22,8 @@
color: @text-color-very-subtle;
font-weight: @font-weight-semi-bold;
font-size: @font-size-small * 0.9;
- padding-left:@padding-small-horizontal;
- padding-top:@padding-small-horizontal;
+ padding-left: @padding-small-horizontal;
+ padding-top: @padding-small-horizontal;
padding-right: @padding-xs-horizontal;
letter-spacing: -0.2px;
@@ -37,7 +37,9 @@
margin-left: @padding-small-horizontal;
margin-right: @padding-small-horizontal;
cursor: pointer;
- img {background: @text-color-very-subtle; }
+ img {
+ background: @text-color-very-subtle;
+ }
display: none;
}
@@ -52,19 +54,22 @@
&:hover {
.heading {
- .collapse-button,.add-item-button {
+ .collapse-button,
+ .add-item-button {
display: inherit;
}
}
}
.item-container {
- display:flex;
+ display: flex;
&.dropping {
background-color: lighten(@source-list-bg, 20%);
.item {
color: @source-list-active-color;
- img.content-mask { background-color: @source-list-active-color; }
+ img.content-mask {
+ background-color: @source-list-active-color;
+ }
}
}
}
@@ -106,7 +111,8 @@
font-weight: @font-weight-semi-bold;
color: @count-color;
margin-left: @padding-small-horizontal * 0.8;
- box-shadow: 0 0.5px 0 @count-color, 0 -0.5px 0 @count-color, 0.5px 0 0 @count-color, -0.5px 0 0 @count-color;
+ box-shadow: 0 0.5px 0 @count-color, 0 -0.5px 0 @count-color, 0.5px 0 0 @count-color,
+ -0.5px 0 0 @count-color;
}
.item-count-box.alt-count {
color: @source-list-active-bg;
@@ -122,12 +128,16 @@
&.selected {
background: @source-list-active-bg;
color: @source-list-active-color;
- img.content-mask { background-color: @source-list-active-color; }
+ img.content-mask {
+ background-color: @source-list-active-color;
+ }
}
&.dropping {
background-color: lighten(@source-list-bg, 20%);
color: @source-list-active-color;
- img.content-mask { background-color: @source-list-active-color; }
+ img.content-mask {
+ background-color: @source-list-active-color;
+ }
}
&.editing {
align-items: center;
diff --git a/app/static/workspace.less b/app/static/workspace.less
index 477716572..aa0e16a02 100644
--- a/app/static/workspace.less
+++ b/app/static/workspace.less
@@ -61,8 +61,8 @@ mailspring-workspace {
.toolbar-window-controls {
margin-top: 9px;
- margin-left: @spacing-half;
- order: -1000;
+ margin-left: @spacing-half /*rtl:ignore */;
+ order: -1000 /*rtl: 1000*/;
min-width: 72px;
width: 72px;
flex-grow: 0;
@@ -76,12 +76,11 @@ mailspring-workspace {
button {
-webkit-app-region: no-drag;
- display: inline-block;
padding: 0;
width: 12px;
height: 12px;
margin: 4px;
- float: left;
+ float: left /*rtl:left */;
background-color: transparent;
background-repeat: no-repeat;
background-position: 0 0;
@@ -395,7 +394,7 @@ body.platform-linux,
body.platform-win32.window-type-default {
.toolbar-menu-control {
display: inherit;
- order: 10000;
+ order: 10000 /*rtl: -10000 */;
.btn-toolbar {
margin-left: 0;
padding: 0 17px;
diff --git a/package-lock.json b/package-lock.json
index 3cd299e23..6b7a32eb9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1788,7 +1788,7 @@
},
"minimist": {
"version": "1.2.0",
- "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
}