diff --git a/app/internal_packages/activity/lib/dashboard/metrics-components.jsx b/app/internal_packages/activity/lib/dashboard/metrics-components.jsx index 46ef5c3ce..e7709e156 100644 --- a/app/internal_packages/activity/lib/dashboard/metrics-components.jsx +++ b/app/internal_packages/activity/lib/dashboard/metrics-components.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { RetinaImg } from 'mailspring-component-kit'; -import { localized } from 'mailspring-exports'; +import { localized, isRTL } from 'mailspring-exports'; export class MetricContainer extends React.Component { render() { @@ -24,7 +24,7 @@ export class MetricStat extends React.Component { style={{ zIndex: 1, padding: `15px 5px`, - textAlign: 'right', + textAlign: isRTL ? 'left' : 'right', }} > diff --git a/app/internal_packages/activity/lib/dashboard/root.jsx b/app/internal_packages/activity/lib/dashboard/root.jsx index c7a50d806..97f1f4aa5 100644 --- a/app/internal_packages/activity/lib/dashboard/root.jsx +++ b/app/internal_packages/activity/lib/dashboard/root.jsx @@ -423,17 +423,13 @@ class RootWithTimespan extends React.Component {
-
+
{localized('Learn More')}
{localized('Export Raw Data')}
diff --git a/app/internal_packages/activity/styles/dashboard.less b/app/internal_packages/activity/styles/dashboard.less index 67f3a2308..8730eeb03 100644 --- a/app/internal_packages/activity/styles/dashboard.less +++ b/app/internal_packages/activity/styles/dashboard.less @@ -199,6 +199,8 @@ .metric-graph { position: relative; height: 120px; + transform: scaleX(1) /*rtl: scaleX(-1) */; + .gridline { width: 1; height: 100%; @@ -207,10 +209,11 @@ } .layer.text-overlay { background: linear-gradient( - to right, - fadeout(@background-primary, 0%) 15%, - fadeout(@background-primary, 100%) 100% - ); + to right, + fadeout(@background-primary, 0%) 15%, + fadeout(@background-primary, 100%) 100% + ) + /*rtl:ignore */; font-size: 60px; display: flex; align-items: flex-end; diff --git a/app/internal_packages/preferences/lib/tabs/preferences-account-list.jsx b/app/internal_packages/preferences/lib/tabs/preferences-account-list.jsx index 457d9a382..a28721463 100644 --- a/app/internal_packages/preferences/lib/tabs/preferences-account-list.jsx +++ b/app/internal_packages/preferences/lib/tabs/preferences-account-list.jsx @@ -49,9 +49,11 @@ class PreferencesAccountList extends Component { mode={RetinaImg.Mode.ContentPreserve} />
-
-
{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 (
- +
{title}
); @@ -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 }