RTL interface support for Arabic, etc. Flipped UI, fine tuning via RTLCSS

This commit is contained in:
Ben Gotow 2018-10-15 22:45:24 -07:00
parent 9c8e09ab85
commit 1142b59f72
17 changed files with 242 additions and 44 deletions

View file

@ -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',
}}
>
<RetinaImg name={`metric-background-${name}.png`} mode={RetinaImg.Mode.ContentIsMask} />

View file

@ -423,17 +423,13 @@ class RootWithTimespan extends React.Component {
</div>
<div className="section hidden-on-web" style={{ display: 'flex', textAlign: 'center' }}>
<div style={{ display: 'flex', margin: 'auto' }}>
<div
className="btn"
onClick={this._onLearnMore}
style={{ marginRight: 10, minWidth: 115 }}
>
<div className="btn" onClick={this._onLearnMore} style={{ minWidth: 115 }}>
{localized('Learn More')}
</div>
<div
className="btn"
onClick={this._onExport}
style={{ marginRight: 10, minWidth: 135 }}
style={{ marginRight: 10, marginLeft: 10, minWidth: 135 }}
>
{localized('Export Raw Data')}
</div>

View file

@ -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;

View file

@ -49,9 +49,11 @@ class PreferencesAccountList extends Component {
mode={RetinaImg.Mode.ContentPreserve}
/>
</div>
<div style={{ flex: 1, marginLeft: 10 }}>
<div className="account-name">{label}</div>
<div className="account-subtext">
<div style={{ flex: 1, marginLeft: 10, marginRight: 10 }}>
<div className="account-name" dir="auto">
{label}
</div>
<div className="account-subtext" dir="auto">
{accountSub} ({account.displayProvider()})
</div>
</div>

View file

@ -120,8 +120,12 @@ const c3 = new ListTabular.Column({
return (
<span className="details">
<MailLabelSet thread={thread} />
<span className="subject">{subject(thread.subject)}</span>
<span className="snippet">{getSnippet(thread)}</span>
<span className="subject" dir="auto">
{subject(thread.subject)}
</span>
<span className="snippet" dir="auto">
{getSnippet(thread)}
</span>
{attachment}
</span>
);
@ -221,9 +225,13 @@ const cNarrow = new ListTabular.Column({
matching={{ role: 'ThreadListTimestamp' }}
/>
</div>
<div className="subject">{subject(thread.subject)}</div>
<div className="subject" dir="auto">
{subject(thread.subject)}
</div>
<div className="snippet-and-labels">
<div className="snippet">{getSnippet(thread)}&nbsp;</div>
<div className="snippet" dir="auto">
{getSnippet(thread)}&nbsp;
</div>
<div style={{ flex: 1, flexShrink: 1 }} />
<MailLabelSet thread={thread} />
</div>

View file

@ -14,7 +14,11 @@ class ThreadListParticipants extends React.Component {
render() {
const items = this.getTokens();
return <div className="participants">{this.renderSpans(items)}</div>;
return (
<div className="participants" dir="auto">
{this.renderSpans(items)}
</div>
);
}
renderSpans(items) {

122
app/package-lock.json generated
View file

@ -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": {

View file

@ -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",

View file

@ -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');

View file

@ -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

View file

@ -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');

View file

@ -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`)

View file

@ -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 +)

View file

@ -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 (
<div className="item-back" onClick={this._onClick} title={localized(`Return to %@`, title)}>
<RetinaImg name="sheet-back.png" mode={RetinaImg.Mode.ContentIsMask} />
<RetinaImg
name="sheet-back.png"
mode={RetinaImg.Mode.ContentIsMask}
style={isRTL ? { transform: `scaleX(-1)` } : {}}
/>
<div className="item-back-title">{title}</div>
</div>
);
@ -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 {

View file

@ -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;

View file

@ -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;

2
package-lock.json generated
View file

@ -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
}