summaryrefslogtreecommitdiffhomepage
path: root/gui/src/renderer
diff options
context:
space:
mode:
Diffstat (limited to 'gui/src/renderer')
-rw-r--r--gui/src/renderer/app.tsx131
-rw-r--r--gui/src/renderer/components/AdvancedSettings.tsx68
-rw-r--r--gui/src/renderer/components/Cell.tsx24
-rw-r--r--gui/src/renderer/components/CityRow.tsx11
-rw-r--r--gui/src/renderer/components/CountryRow.tsx11
-rw-r--r--gui/src/renderer/components/LocationList.tsx13
-rw-r--r--gui/src/renderer/components/Login.tsx2
-rw-r--r--gui/src/renderer/components/RelayRow.tsx9
-rw-r--r--gui/src/renderer/components/RelayStatusIndicator.tsx8
-rw-r--r--gui/src/renderer/components/SelectLanguage.tsx119
-rw-r--r--gui/src/renderer/components/Selector.tsx82
-rw-r--r--gui/src/renderer/components/Settings.tsx57
-rw-r--r--gui/src/renderer/components/SettingsStyles.tsx23
-rw-r--r--gui/src/renderer/containers/AccountPage.tsx4
-rw-r--r--gui/src/renderer/containers/ConnectPage.tsx4
-rw-r--r--gui/src/renderer/containers/SelectLanguagePage.tsx31
-rw-r--r--gui/src/renderer/containers/SettingsPage.tsx6
-rw-r--r--gui/src/renderer/containers/WireguardKeysPage.tsx4
-rw-r--r--gui/src/renderer/redux/settings/reducers.ts1
-rw-r--r--gui/src/renderer/redux/userinterface/actions.ts34
-rw-r--r--gui/src/renderer/redux/userinterface/reducers.ts10
-rw-r--r--gui/src/renderer/routes.tsx3
-rw-r--r--gui/src/renderer/transitions.ts1
23 files changed, 467 insertions, 189 deletions
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx
index 39e6f56fda..2613aacb88 100644
--- a/gui/src/renderer/app.tsx
+++ b/gui/src/renderer/app.tsx
@@ -23,7 +23,7 @@ import versionActions from './redux/version/actions';
import { IAppUpgradeInfo, ICurrentAppVersionInfo } from '../main';
import { cities, countries, loadTranslations, messages, relayLocations } from '../shared/gettext';
-import { IGuiSettingsState } from '../shared/gui-settings-state';
+import { IGuiSettingsState, SYSTEM_PREFERRED_LOCALE_KEY } from '../shared/gui-settings-state';
import { IpcRendererEventChannel, IRelayListPair } from '../shared/ipc-event-channel';
import { getRendererLogFile, setupLogging } from '../shared/logging';
@@ -43,6 +43,28 @@ import {
TunnelState,
} from '../shared/daemon-rpc-types';
+interface IPreferredLocaleDescriptor {
+ name: string;
+ code: string;
+}
+
+const SUPPORTED_LOCALE_LIST = [
+ { name: 'Deutsche', code: 'de' },
+ { name: 'English', code: 'en' },
+ { name: 'Español', code: 'es' },
+ { name: 'Français', code: 'fr' },
+ { name: 'Italiano', code: 'it' },
+ { name: '日本語', code: 'ja' },
+ { name: 'Nederlands', code: 'nl' },
+ { name: 'Norsk', code: 'no' },
+ { name: 'Português', code: 'pt' },
+ { name: 'Русский', code: 'ru' },
+ { name: 'Svenska', code: 'sv' },
+ { name: 'Türkçe', code: 'tr' },
+ { name: '简体中文', code: 'zh-CN' },
+ { name: '繁體中文', code: 'zh-TW' },
+];
+
export default class AppRenderer {
private memoryHistory = createMemoryHistory();
private reduxStore = configureStore(this.memoryHistory);
@@ -61,10 +83,12 @@ export default class AppRenderer {
),
};
- private locale: string;
- private tunnelState: TunnelState;
- private settings: ISettings;
- private guiSettings: IGuiSettingsState;
+ private locale = 'en';
+ private location?: ILocation;
+ private relayListPair!: IRelayListPair;
+ private tunnelState!: TunnelState;
+ private settings!: ISettings;
+ private guiSettings!: IGuiSettingsState;
private connectedToDaemon = false;
private autoConnected = false;
private doingLogin = false;
@@ -73,6 +97,20 @@ export default class AppRenderer {
constructor() {
setupLogging(getRendererLogFile());
+ IpcRendererEventChannel.locale.listen((locale) => {
+ // load translations for the new locale
+ this.loadTranslations(locale);
+
+ // set current locale
+ this.setLocale(locale);
+
+ // refresh the relay list pair with the new translations
+ this.propagateRelayListPairToRedux();
+
+ // refresh the location with the new translations
+ this.propagateLocationToRedux();
+ });
+
IpcRendererEventChannel.windowShape.listen((windowShapeParams) => {
if (typeof windowShapeParams.arrowPosition === 'number') {
this.reduxActions.userInterface.updateWindowArrowPosition(windowShapeParams.arrowPosition);
@@ -113,8 +151,7 @@ export default class AppRenderer {
});
IpcRendererEventChannel.relays.listen((relayListPair: IRelayListPair) => {
- this.setRelays(relayListPair.relays);
- this.setBridges(relayListPair.bridges);
+ this.setRelayListPair(relayListPair);
});
IpcRendererEventChannel.currentVersion.listen((currentVersion: ICurrentAppVersionInfo) => {
@@ -144,15 +181,9 @@ export default class AppRenderer {
// Request the initial state from the main process
const initialState = IpcRendererEventChannel.state.get();
- this.locale = initialState.locale;
- this.tunnelState = initialState.tunnelState;
- this.settings = initialState.settings;
- this.guiSettings = initialState.guiSettings;
-
// Load translations
- for (const catalogue of [messages, countries, cities, relayLocations]) {
- loadTranslations(this.locale, catalogue);
- }
+ this.loadTranslations(initialState.locale);
+ this.setLocale(initialState.locale);
this.setAccountExpiry(initialState.accountData && initialState.accountData.expiry);
this.setAccountHistory(initialState.accountHistory);
@@ -164,8 +195,7 @@ export default class AppRenderer {
this.setLocation(initialState.location);
}
- this.setRelays(initialState.relays);
- this.setBridges(initialState.bridges);
+ this.setRelayListPair(initialState.relayListPair);
this.setCurrentVersion(initialState.currentVersion);
this.setUpgradeVersion(initialState.upgradeVersion);
this.setGuiSettings(initialState.guiSettings);
@@ -185,12 +215,7 @@ export default class AppRenderer {
<Provider store={this.reduxStore}>
<ConnectedRouter history={this.memoryHistory}>
<ErrorBoundary>
- <AppRoutes
- sharedProps={{
- app: this,
- locale: this.locale,
- }}
- />
+ <AppRoutes sharedProps={{ app: this }} />
</ErrorBoundary>
</ConnectedRouter>
</Provider>
@@ -338,6 +363,39 @@ export default class AppRenderer {
actions.settings.setWireguardKeygenEvent(keygenEvent);
}
+ public getPreferredLocaleList(): IPreferredLocaleDescriptor[] {
+ return [
+ {
+ // TRANSLATORS: The option that represents the active operating system language in the
+ // TRANSLATORS: user interface language selection list.
+ name: messages.gettext('System default'),
+ code: SYSTEM_PREFERRED_LOCALE_KEY,
+ },
+ ...SUPPORTED_LOCALE_LIST,
+ ];
+ }
+
+ public setPreferredLocale(preferredLocale: string) {
+ IpcRendererEventChannel.guiSettings.setPreferredLocale(preferredLocale);
+ }
+
+ private getPreferredLocaleDisplayName(localeCode: string): string {
+ const preferredLocale = this.getPreferredLocaleList().find((item) => item.code === localeCode);
+
+ return preferredLocale ? preferredLocale.name : '';
+ }
+
+ private loadTranslations(locale: string) {
+ for (const catalogue of [messages, countries, cities, relayLocations]) {
+ loadTranslations(locale, catalogue);
+ }
+ }
+
+ private setLocale(locale: string) {
+ this.locale = locale;
+ this.reduxActions.userInterface.updateLocale(locale);
+ }
+
private setRelaySettings(relaySettings: RelaySettings) {
const actions = this.reduxActions;
@@ -546,7 +604,14 @@ export default class AppRenderer {
}
private setLocation(location: ILocation) {
- this.reduxActions.connection.newLocation(this.translateLocation(location));
+ this.location = location;
+ this.propagateLocationToRedux();
+ }
+
+ private propagateLocationToRedux() {
+ if (this.location) {
+ this.reduxActions.connection.newLocation(this.translateLocation(this.location));
+ }
}
private translateLocation(inputLocation: ILocation): ILocation {
@@ -589,16 +654,17 @@ export default class AppRenderer {
.sort((countryA, countryB) => countryA.name.localeCompare(countryB.name, this.locale));
}
- private setRelays(relayList: IRelayList) {
- const locations = this.convertRelayListToLocationList(relayList);
-
- this.reduxActions.settings.updateRelayLocations(locations);
+ private setRelayListPair(relayListPair: IRelayListPair) {
+ this.relayListPair = relayListPair;
+ this.propagateRelayListPairToRedux();
}
- private setBridges(relayList: IRelayList) {
- const locations = this.convertRelayListToLocationList(relayList);
+ private propagateRelayListPairToRedux() {
+ const relays = this.convertRelayListToLocationList(this.relayListPair.relays);
+ const bridges = this.convertRelayListToLocationList(this.relayListPair.relays);
- this.reduxActions.settings.updateBridgeLocations(locations);
+ this.reduxActions.settings.updateRelayLocations(relays);
+ this.reduxActions.settings.updateBridgeLocations(bridges);
}
private setCurrentVersion(versionInfo: ICurrentAppVersionInfo) {
@@ -612,6 +678,9 @@ export default class AppRenderer {
private setGuiSettings(guiSettings: IGuiSettingsState) {
this.guiSettings = guiSettings;
this.reduxActions.settings.updateGuiSettings(guiSettings);
+ this.reduxActions.userInterface.updatePreferredLocaleName(
+ this.getPreferredLocaleDisplayName(guiSettings.preferredLocale),
+ );
}
private setAccountExpiry(expiry?: string) {
diff --git a/gui/src/renderer/components/AdvancedSettings.tsx b/gui/src/renderer/components/AdvancedSettings.tsx
index c9a985789b..53c0feb4bd 100644
--- a/gui/src/renderer/components/AdvancedSettings.tsx
+++ b/gui/src/renderer/components/AdvancedSettings.tsx
@@ -1,7 +1,6 @@
import * as React from 'react';
import { Component, View } from 'reactxp';
import { sprintf } from 'sprintf-js';
-import { colors } from '../../config.json';
import { BridgeState, RelayProtocol, TunnelProtocol } from '../../shared/daemon-rpc-types';
import { messages } from '../../shared/gettext';
import styles from './AdvancedSettingsStyles';
@@ -15,6 +14,7 @@ import {
NavigationScrollbars,
TitleBarItem,
} from './NavigationBar';
+import Selector, { ISelectorItem } from './Selector';
import SettingsHeader, { HeaderTitle } from './SettingsHeader';
const MIN_MSSFIX_VALUE = 1000;
@@ -404,69 +404,3 @@ export default class AdvancedSettings extends Component<IProps, IState> {
return mssfix === undefined || (mssfix >= MIN_MSSFIX_VALUE && mssfix <= MAX_MSSFIX_VALUE);
}
}
-
-interface ISelectorItem<T> {
- label: string;
- value: T;
-}
-
-interface ISelectorProps<T> {
- title: string;
- values: Array<ISelectorItem<T>>;
- value: T;
- onSelect: (value: T) => void;
-}
-
-class Selector<T> extends Component<ISelectorProps<T>> {
- public render() {
- return (
- <Cell.Section style={styles.advanced_settings__selector_section}>
- <Cell.SectionTitle>{this.props.title}</Cell.SectionTitle>
- {this.props.values.map((item, i) => (
- <SelectorCell
- key={i}
- value={item.value}
- selected={item.value === this.props.value}
- onSelect={this.props.onSelect}>
- {item.label}
- </SelectorCell>
- ))}
- </Cell.Section>
- );
- }
-}
-
-interface ISelectorCell<T> {
- value: T;
- selected: boolean;
- onSelect: (value: T) => void;
- children?: React.ReactText;
-}
-
-class SelectorCell<T> extends Component<ISelectorCell<T>> {
- public render() {
- return (
- <Cell.CellButton
- style={this.props.selected ? styles.advanced_settings__cell_selected_hover : undefined}
- cellHoverStyle={
- this.props.selected ? styles.advanced_settings__cell_selected_hover : undefined
- }
- onPress={this.onPress}>
- <Cell.Icon
- style={this.props.selected ? undefined : styles.advanced_settings__cell_icon_invisible}
- source="icon-tick"
- width={24}
- height={24}
- tintColor={colors.white}
- />
- <Cell.Label>{this.props.children}</Cell.Label>
- </Cell.CellButton>
- );
- }
-
- private onPress = () => {
- if (!this.props.selected) {
- this.props.onSelect(this.props.value);
- }
- };
-}
diff --git a/gui/src/renderer/components/Cell.tsx b/gui/src/renderer/components/Cell.tsx
index 397b7513a0..1693f85c2b 100644
--- a/gui/src/renderer/components/Cell.tsx
+++ b/gui/src/renderer/components/Cell.tsx
@@ -23,6 +23,9 @@ const styles = {
hover: Styles.createButtonStyle({
backgroundColor: colors.blue80,
}),
+ selected: Styles.createViewStyle({
+ backgroundColor: colors.green,
+ }),
},
cellContainer: Styles.createViewStyle({
backgroundColor: colors.blue,
@@ -122,8 +125,9 @@ const styles = {
interface ICellButtonProps {
children?: React.ReactNode;
disabled?: boolean;
- cellHoverStyle?: Types.StyleRuleSetRecursive<Types.ButtonStyleRuleSet>;
+ selected?: boolean;
style?: Types.StyleRuleSetRecursive<Types.ButtonStyleRuleSet>;
+ hoverStyle?: Types.StyleRuleSetRecursive<Types.ButtonStyleRuleSet>;
onPress?: () => void;
}
@@ -135,14 +139,20 @@ const CellSectionContext = React.createContext<boolean>(false);
const CellHoverContext = React.createContext<boolean>(false);
export class CellButton extends Component<ICellButtonProps, IState> {
- public state = { hovered: false };
+ public state: IState = { hovered: false };
public onHoverStart = () => (!this.props.disabled ? this.setState({ hovered: true }) : null);
public onHoverEnd = () => (!this.props.disabled ? this.setState({ hovered: false }) : null);
public render() {
- const { children, style, cellHoverStyle, ...otherProps } = this.props;
- const hoverStyle = cellHoverStyle || styles.cellButton.hover;
+ const { children, style, hoverStyle, ...otherProps } = this.props;
+
+ const stateStyle = this.props.selected
+ ? styles.cellButton.selected
+ : this.state.hovered
+ ? hoverStyle || styles.cellButton.hover
+ : undefined;
+
return (
<CellSectionContext.Consumer>
{(containedInSection) => (
@@ -151,7 +161,7 @@ export class CellButton extends Component<ICellButtonProps, IState> {
styles.cellButton.base,
containedInSection ? styles.cellButton.section : undefined,
style,
- this.state.hovered ? hoverStyle : undefined,
+ stateStyle,
]}
onHoverStart={this.onHoverStart}
onHoverEnd={this.onHoverEnd}
@@ -371,6 +381,10 @@ export const Icon = function CellIcon(props: ImageView['props']) {
);
};
+export const UntintedIcon = function CellIcon(props: ImageView['props']) {
+ return <ImageView {...props} style={[styles.icon, props.style]} />;
+};
+
export const Footer = function CellFooter({ children }: IContainerProps) {
return (
<View style={styles.footer.container}>
diff --git a/gui/src/renderer/components/CityRow.tsx b/gui/src/renderer/components/CityRow.tsx
index 331c6b9fdd..c8041abdbd 100644
--- a/gui/src/renderer/components/CityRow.tsx
+++ b/gui/src/renderer/components/CityRow.tsx
@@ -27,9 +27,6 @@ const styles = {
paddingLeft: 32,
backgroundColor: colors.blue40,
}),
- selected: Styles.createButtonStyle({
- backgroundColor: colors.green,
- }),
};
export default class CityRow extends Component<IProps> {
@@ -75,11 +72,11 @@ export default class CityRow extends Component<IProps> {
<Cell.CellButton
onPress={this.handlePress}
disabled={!this.props.hasActiveRelays}
- cellHoverStyle={this.props.selected ? styles.selected : undefined}
- style={[styles.base, this.props.selected ? styles.selected : undefined]}>
+ selected={this.props.selected}
+ style={styles.base}>
<RelayStatusIndicator
- isActive={this.props.hasActiveRelays}
- isSelected={this.props.selected}
+ active={this.props.hasActiveRelays}
+ selected={this.props.selected}
/>
<Cell.Label>{this.props.name}</Cell.Label>
diff --git a/gui/src/renderer/components/CountryRow.tsx b/gui/src/renderer/components/CountryRow.tsx
index 54e1003736..46f40694c9 100644
--- a/gui/src/renderer/components/CountryRow.tsx
+++ b/gui/src/renderer/components/CountryRow.tsx
@@ -1,6 +1,5 @@
import * as React from 'react';
import { Component, Styles, Types, View } from 'reactxp';
-import { colors } from '../../config.json';
import { compareRelayLocation, RelayLocation } from '../../shared/daemon-rpc-types';
import Accordion from './Accordion';
import * as Cell from './Cell';
@@ -30,9 +29,6 @@ const styles = {
paddingRight: 0,
paddingLeft: 16,
}),
- selected: Styles.createViewStyle({
- backgroundColor: colors.green,
- }),
};
export default class CountryRow extends Component<IProps> {
@@ -82,13 +78,12 @@ export default class CountryRow extends Component<IProps> {
return (
<View style={styles.container}>
<Cell.CellButton
- cellHoverStyle={this.props.selected ? styles.selected : undefined}
- style={[styles.base, this.props.selected ? styles.selected : undefined]}
+ style={styles.base}
onPress={this.handlePress}
disabled={!this.props.hasActiveRelays}>
<RelayStatusIndicator
- isActive={this.props.hasActiveRelays}
- isSelected={this.props.selected}
+ active={this.props.hasActiveRelays}
+ selected={this.props.selected}
/>
<Cell.Label>{this.props.name}</Cell.Label>
{hasChildren ? (
diff --git a/gui/src/renderer/components/LocationList.tsx b/gui/src/renderer/components/LocationList.tsx
index fb1e7ae8f9..a86ae25897 100644
--- a/gui/src/renderer/components/LocationList.tsx
+++ b/gui/src/renderer/components/LocationList.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import { Component, Styles, View } from 'reactxp';
+import { Component, View } from 'reactxp';
import { colors } from '../../config.json';
import {
compareRelayLocation,
@@ -13,12 +13,6 @@ import CityRow from './CityRow';
import CountryRow from './CountryRow';
import RelayRow from './RelayRow';
-const styles = {
- selectedCell: Styles.createViewStyle({
- backgroundColor: colors.green,
- }),
-};
-
export enum LocationSelectionType {
relay = 'relay',
special = 'special',
@@ -226,10 +220,7 @@ interface ISpecialLocationProps<T> {
export class SpecialLocation<T> extends Component<ISpecialLocationProps<T>> {
public render() {
return (
- <Cell.CellButton
- style={this.props.isSelected ? styles.selectedCell : undefined}
- cellHoverStyle={this.props.isSelected ? styles.selectedCell : undefined}
- onPress={this.onSelect}>
+ <Cell.CellButton selected={this.props.isSelected} onPress={this.onSelect}>
<Cell.Icon
source={this.props.isSelected ? 'icon-tick' : this.props.icon}
tintColor={colors.white}
diff --git a/gui/src/renderer/components/Login.tsx b/gui/src/renderer/components/Login.tsx
index 4773e40dbc..9e2ff82433 100644
--- a/gui/src/renderer/components/Login.tsx
+++ b/gui/src/renderer/components/Login.tsx
@@ -449,7 +449,7 @@ class AccountDropdownItem extends Component<IAccountDropdownItemProps> {
<View style={styles.account_dropdown__spacer} />
<Cell.CellButton
style={styles.account_dropdown__item}
- cellHoverStyle={styles.account_dropdown__item_hover}>
+ hoverStyle={styles.account_dropdown__item_hover}>
<Cell.Label
textStyle={styles.account_dropdown__label}
containerStyle={styles.account_dropdown__label_container}
diff --git a/gui/src/renderer/components/RelayRow.tsx b/gui/src/renderer/components/RelayRow.tsx
index 35cb1d2be3..cd87610944 100644
--- a/gui/src/renderer/components/RelayRow.tsx
+++ b/gui/src/renderer/components/RelayRow.tsx
@@ -19,9 +19,6 @@ const styles = {
paddingLeft: 48,
backgroundColor: colors.blue20,
}),
- selected: Styles.createViewStyle({
- backgroundColor: colors.green,
- }),
};
export default class RelayRow extends Component<IProps> {
@@ -42,10 +39,10 @@ export default class RelayRow extends Component<IProps> {
return (
<Cell.CellButton
onPress={this.handlePress}
- cellHoverStyle={this.props.selected ? styles.selected : undefined}
+ selected={this.props.selected}
disabled={!this.props.active}
- style={[styles.base, this.props.selected ? styles.selected : undefined]}>
- <RelayStatusIndicator isActive={this.props.active} isSelected={this.props.selected} />
+ style={styles.base}>
+ <RelayStatusIndicator active={this.props.active} selected={this.props.selected} />
<Cell.Label>{this.props.hostname}</Cell.Label>
</Cell.CellButton>
diff --git a/gui/src/renderer/components/RelayStatusIndicator.tsx b/gui/src/renderer/components/RelayStatusIndicator.tsx
index 594dbec141..26c7f616a1 100644
--- a/gui/src/renderer/components/RelayStatusIndicator.tsx
+++ b/gui/src/renderer/components/RelayStatusIndicator.tsx
@@ -20,16 +20,16 @@ const styles = {
};
interface IProps {
- isActive: boolean;
- isSelected: boolean;
+ active: boolean;
+ selected: boolean;
}
export default class RelayStatusIndicator extends Component<IProps> {
public render() {
- return this.props.isSelected ? (
+ return this.props.selected ? (
<Cell.Icon tintColor={colors.white} source="icon-tick" height={24} width={24} />
) : (
- <View style={[styles.relayStatus, this.props.isActive ? styles.active : styles.inactive]} />
+ <View style={[styles.relayStatus, this.props.active ? styles.active : styles.inactive]} />
);
}
}
diff --git a/gui/src/renderer/components/SelectLanguage.tsx b/gui/src/renderer/components/SelectLanguage.tsx
new file mode 100644
index 0000000000..c5c2228d00
--- /dev/null
+++ b/gui/src/renderer/components/SelectLanguage.tsx
@@ -0,0 +1,119 @@
+import * as React from 'react';
+import ReactDOM from 'react-dom';
+import { Component, Styles, View } from 'reactxp';
+import { colors } from '../../config.json';
+import { messages } from '../../shared/gettext';
+import CustomScrollbars from './CustomScrollbars';
+import { Container, Layout } from './Layout';
+import {
+ BackBarItem,
+ NavigationBar,
+ NavigationContainer,
+ NavigationItems,
+ NavigationScrollbars,
+ TitleBarItem,
+} from './NavigationBar';
+import Selector, { ISelectorItem, SelectorCell } from './Selector';
+import SettingsHeader, { HeaderTitle } from './SettingsHeader';
+
+interface IProps {
+ preferredLocale: string;
+ preferredLocalesList: Array<{ name: string; code: string }>;
+ setPreferredLocale: (locale: string) => void;
+ onClose: () => void;
+}
+
+interface IState {
+ source: Array<ISelectorItem<string>>;
+}
+
+const styles = {
+ page: Styles.createViewStyle({
+ backgroundColor: colors.darkBlue,
+ flex: 1,
+ }),
+ container: Styles.createViewStyle({
+ flex: 1,
+ }),
+ selector: Styles.createViewStyle({
+ marginBottom: 0,
+ }),
+ // plain CSS style
+ scrollview: {
+ flex: 1,
+ },
+};
+
+export default class SelectLanguage extends Component<IProps, IState> {
+ private scrollView = React.createRef<CustomScrollbars>();
+ private selectedCellRef = React.createRef<SelectorCell<string>>();
+
+ constructor(props: IProps) {
+ super(props);
+
+ this.state = {
+ source: [
+ ...this.props.preferredLocalesList.map((item) => ({ label: item.name, value: item.code })),
+ ],
+ };
+ }
+
+ public componentDidMount() {
+ this.scrollToSelectedCell();
+ }
+
+ public render() {
+ return (
+ <Layout>
+ <Container>
+ <View style={styles.page}>
+ <NavigationContainer>
+ <NavigationBar>
+ <NavigationItems>
+ <BackBarItem action={this.props.onClose}>
+ {// TRANSLATORS: Back button in navigation bar
+ messages.pgettext('select-language-nav', 'Settings')}
+ </BackBarItem>
+ <TitleBarItem>
+ {// TRANSLATORS: Title label in navigation bar
+ messages.pgettext('select-language-nav', 'Select language')}
+ </TitleBarItem>
+ </NavigationItems>
+ </NavigationBar>
+
+ <View style={styles.container}>
+ <NavigationScrollbars style={styles.scrollview}>
+ <SettingsHeader>
+ <HeaderTitle>
+ {messages.pgettext('select-language-nav', 'Select language')}
+ </HeaderTitle>
+ </SettingsHeader>
+ <Selector
+ style={styles.selector}
+ title=""
+ values={this.state.source}
+ value={this.props.preferredLocale}
+ onSelect={this.props.setPreferredLocale}
+ selectedCellRef={this.selectedCellRef}
+ />
+ </NavigationScrollbars>
+ </View>
+ </NavigationContainer>
+ </View>
+ </Container>
+ </Layout>
+ );
+ }
+
+ private scrollToSelectedCell() {
+ const ref = this.selectedCellRef.current;
+ const scrollView = this.scrollView.current;
+
+ if (scrollView && ref) {
+ const cellDOMNode = ReactDOM.findDOMNode(ref);
+ if (cellDOMNode instanceof HTMLElement) {
+ scrollView.scrollToElement(cellDOMNode, 'middle');
+ }
+ }
+ }
+}
diff --git a/gui/src/renderer/components/Selector.tsx b/gui/src/renderer/components/Selector.tsx
new file mode 100644
index 0000000000..1bbc00ba9a
--- /dev/null
+++ b/gui/src/renderer/components/Selector.tsx
@@ -0,0 +1,82 @@
+import * as React from 'react';
+import { Component, Styles, Types, View } from 'reactxp';
+import { colors } from '../../config.json';
+import * as Cell from './Cell';
+
+export interface ISelectorItem<T> {
+ label: string;
+ value: T;
+}
+
+interface ISelectorProps<T> {
+ style?: Types.ViewStyleRuleSet;
+ title?: string;
+ values: Array<ISelectorItem<T>>;
+ value: T;
+ onSelect: (value: T) => void;
+ selectedCellRef?: React.Ref<SelectorCell<T>>;
+}
+
+const styles = {
+ section: Styles.createViewStyle({
+ marginBottom: 24,
+ }),
+ invisibleIcon: Styles.createViewStyle({
+ opacity: 0,
+ }),
+};
+
+export default class Selector<T> extends Component<ISelectorProps<T>> {
+ public render() {
+ const items = this.props.values.map((item, i) => {
+ const selected = item.value === this.props.value;
+
+ return (
+ <SelectorCell
+ key={i}
+ value={item.value}
+ selected={selected}
+ ref={selected ? this.props.selectedCellRef : undefined}
+ onSelect={this.props.onSelect}>
+ {item.label}
+ </SelectorCell>
+ );
+ });
+
+ if (this.props.title) {
+ return <Cell.Section style={[styles.section, this.props.style]}>{items}</Cell.Section>;
+ } else {
+ return <View style={[styles.section, this.props.style]}>{items}</View>;
+ }
+ }
+}
+
+interface ISelectorCellProps<T> {
+ value: T;
+ selected: boolean;
+ onSelect: (value: T) => void;
+ children?: React.ReactText;
+}
+
+export class SelectorCell<T> extends Component<ISelectorCellProps<T>> {
+ public render() {
+ return (
+ <Cell.CellButton onPress={this.onPress} selected={this.props.selected}>
+ <Cell.Icon
+ style={this.props.selected ? undefined : styles.invisibleIcon}
+ source="icon-tick"
+ width={24}
+ height={24}
+ tintColor={colors.white}
+ />
+ <Cell.Label>{this.props.children}</Cell.Label>
+ </Cell.CellButton>
+ );
+ }
+
+ private onPress = () => {
+ if (!this.props.selected) {
+ this.props.onSelect(this.props.value);
+ }
+ };
+}
diff --git a/gui/src/renderer/components/Settings.tsx b/gui/src/renderer/components/Settings.tsx
index 6c04d9a8e5..129550f3a0 100644
--- a/gui/src/renderer/components/Settings.tsx
+++ b/gui/src/renderer/components/Settings.tsx
@@ -5,7 +5,6 @@ import { messages } from '../../shared/gettext';
import AccountExpiry from '../lib/account-expiry';
import * as AppButton from './AppButton';
import * as Cell from './Cell';
-import ImageView from './ImageView';
import { Container, Layout } from './Layout';
import {
CloseBarItem,
@@ -21,6 +20,7 @@ import styles from './SettingsStyles';
import { LoginState } from '../redux/account/reducers';
export interface IProps {
+ preferredLocaleDisplayName: string;
loginState: LoginState;
accountExpiry?: string;
expiryLocale: string;
@@ -30,6 +30,7 @@ export interface IProps {
isOffline: boolean;
onQuit: () => void;
onClose: () => void;
+ onViewSelectLanguage: () => void;
onViewAccount: () => void;
onViewSupport: () => void;
onViewPreferences: () => void;
@@ -39,12 +40,14 @@ export interface IProps {
export default class Settings extends Component<IProps> {
public render() {
+ const showLargeTitle = this.props.loginState !== 'ok';
+
return (
<Layout>
<Container>
<View style={styles.settings}>
<NavigationContainer>
- <NavigationBar>
+ <NavigationBar alwaysDisplayBarTitle={!showLargeTitle}>
<NavigationItems>
<CloseBarItem action={this.props.onClose} />
<TitleBarItem>
@@ -54,12 +57,14 @@ export default class Settings extends Component<IProps> {
</NavigationItems>
</NavigationBar>
- <View style={styles.settings__container}>
- <NavigationScrollbars style={styles.settings__scrollview}>
- <View style={styles.settings__content}>
- <SettingsHeader>
- <HeaderTitle>{messages.pgettext('settings-view', 'Settings')}</HeaderTitle>
- </SettingsHeader>
+ <View style={styles.container}>
+ <NavigationScrollbars style={styles.scrollview}>
+ <View style={styles.content}>
+ {showLargeTitle && (
+ <SettingsHeader>
+ <HeaderTitle>{messages.pgettext('settings-view', 'Settings')}</HeaderTitle>
+ </SettingsHeader>
+ )}
<View>
{this.renderTopButtons()}
{this.renderMiddleButtons()}
@@ -76,9 +81,12 @@ export default class Settings extends Component<IProps> {
);
}
+ private openDownloadLink = () => this.props.onExternalLink(links.download);
+ private openFaqLink = () => this.props.onExternalLink(links.faq);
+
private renderQuitButton() {
return (
- <View style={styles.settings__footer}>
+ <View style={styles.quitButtonFooter}>
<AppButton.RedButton onPress={this.props.onQuit}>
{messages.pgettext('settings-view', 'Quit app')}
</AppButton.RedButton>
@@ -105,8 +113,7 @@ export default class Settings extends Component<IProps> {
<View>
<Cell.CellButton onPress={this.props.onViewAccount}>
<Cell.Label>{messages.pgettext('settings-view', 'Account')}</Cell.Label>
- <Cell.SubText
- style={isOutOfTime ? styles.settings__account_paid_until_label__error : undefined}>
+ <Cell.SubText style={isOutOfTime ? styles.accountPaidUntilErrorLabel : undefined}>
{isOutOfTime ? outOfTimeMessage : formattedExpiry}
</Cell.SubText>
<Cell.Icon height={12} width={7} source="icon-chevron" />
@@ -122,7 +129,7 @@ export default class Settings extends Component<IProps> {
<Cell.Label>{messages.pgettext('settings-view', 'Advanced')}</Cell.Label>
<Cell.Icon height={12} width={7} source="icon-chevron" />
</Cell.CellButton>
- <View style={styles.settings__cell_spacer} />
+ <View style={styles.cellSpacer} />
</View>
);
}
@@ -145,20 +152,14 @@ export default class Settings extends Component<IProps> {
? inconsistentVersionMessage
: updateAvailableMessage;
- icon = (
- <ImageView
- source="icon-alert"
- tintColor={colors.red}
- style={styles.settings__version_warning}
- />
- );
+ icon = <Cell.UntintedIcon source="icon-alert" tintColor={colors.red} />;
footer = (
- <View style={styles.settings__cell_footer}>
- <Text style={styles.settings__cell_footer_label}>{message}</Text>
+ <View style={styles.cellFooter}>
+ <Text style={styles.cellFooterLabel}>{message}</Text>
</View>
);
} else {
- footer = <View style={styles.settings__cell_spacer} />;
+ footer = <View style={styles.cellSpacer} />;
}
return (
@@ -166,7 +167,7 @@ export default class Settings extends Component<IProps> {
<Cell.CellButton disabled={this.props.isOffline} onPress={this.openDownloadLink}>
{icon}
<Cell.Label>{messages.pgettext('settings-view', 'App version')}</Cell.Label>
- <Cell.SubText style={styles.settings__appversion}>{this.props.appVersion}</Cell.SubText>
+ <Cell.SubText style={styles.appVersionLabel}>{this.props.appVersion}</Cell.SubText>
<Cell.Icon height={16} width={16} source="icon-extLink" />
</Cell.CellButton>
{footer}
@@ -174,9 +175,6 @@ export default class Settings extends Component<IProps> {
);
}
- private openDownloadLink = () => this.props.onExternalLink(links.download);
- private openFaqLink = () => this.props.onExternalLink(links.faq);
-
private renderBottomButtons() {
return (
<View>
@@ -189,6 +187,13 @@ export default class Settings extends Component<IProps> {
<Cell.Label>{messages.pgettext('settings-view', 'FAQs & Guides')}</Cell.Label>
<Cell.Icon height={16} width={16} source="icon-extLink" />
</Cell.CellButton>
+
+ <Cell.CellButton onPress={this.props.onViewSelectLanguage}>
+ <Cell.UntintedIcon width={24} height={24} source="icon-language" />
+ <Cell.Label>{messages.pgettext('settings-view', 'Language')}</Cell.Label>
+ <Cell.SubText>{this.props.preferredLocaleDisplayName}</Cell.SubText>
+ <Cell.Icon height={12} width={7} source="icon-chevron" />
+ </Cell.CellButton>
</View>
);
}
diff --git a/gui/src/renderer/components/SettingsStyles.tsx b/gui/src/renderer/components/SettingsStyles.tsx
index 5c9a66fd97..51a215d0fc 100644
--- a/gui/src/renderer/components/SettingsStyles.tsx
+++ b/gui/src/renderer/components/SettingsStyles.tsx
@@ -6,43 +6,40 @@ export default {
backgroundColor: colors.darkBlue,
flex: 1,
}),
- settings__container: Styles.createViewStyle({
+ container: Styles.createViewStyle({
flexDirection: 'column',
flex: 1,
}),
- settings__content: Styles.createViewStyle({
+ content: Styles.createViewStyle({
flexDirection: 'column',
flex: 1,
justifyContent: 'space-between',
overflow: 'visible',
}),
// plain CSS style
- settings__scrollview: {
+ scrollview: {
flex: 1,
},
- settings__cell_spacer: Styles.createViewStyle({
+ cellSpacer: Styles.createViewStyle({
height: 24,
flex: 0,
}),
- settings__cell_footer: Styles.createViewStyle({
+ cellFooter: Styles.createViewStyle({
paddingTop: 8,
paddingRight: 24,
paddingBottom: 24,
paddingLeft: 24,
}),
- settings__footer: Styles.createViewStyle({
+ quitButtonFooter: Styles.createViewStyle({
paddingTop: 24,
- paddingBottom: 24,
+ paddingBottom: 19,
paddingLeft: 24,
paddingRight: 24,
}),
- settings__version_warning: Styles.createViewStyle({
- marginLeft: 8,
- }),
- settings__account_paid_until_label__error: Styles.createTextStyle({
+ accountPaidUntilErrorLabel: Styles.createTextStyle({
color: colors.red,
}),
- settings__cell_footer_label: Styles.createTextStyle({
+ cellFooterLabel: Styles.createTextStyle({
fontFamily: 'Open Sans',
fontSize: 13,
fontWeight: '600',
@@ -50,7 +47,7 @@ export default {
letterSpacing: -0.2,
color: colors.white60,
}),
- settings__appversion: Styles.createTextStyle({
+ appVersionLabel: Styles.createTextStyle({
flex: 0,
}),
};
diff --git a/gui/src/renderer/containers/AccountPage.tsx b/gui/src/renderer/containers/AccountPage.tsx
index 9edd159e1c..ee8588a7c3 100644
--- a/gui/src/renderer/containers/AccountPage.tsx
+++ b/gui/src/renderer/containers/AccountPage.tsx
@@ -8,10 +8,10 @@ import Account from '../components/Account';
import { IReduxState, ReduxDispatch } from '../redux/store';
import { ISharedRouteProps } from '../routes';
-const mapStateToProps = (state: IReduxState, props: ISharedRouteProps) => ({
+const mapStateToProps = (state: IReduxState, _props: ISharedRouteProps) => ({
accountToken: state.account.accountToken,
accountExpiry: state.account.expiry,
- expiryLocale: props.locale,
+ expiryLocale: state.userInterface.locale,
isOffline: state.connection.isBlocked,
});
const mapDispatchToProps = (dispatch: ReduxDispatch, props: ISharedRouteProps) => {
diff --git a/gui/src/renderer/containers/ConnectPage.tsx b/gui/src/renderer/containers/ConnectPage.tsx
index 66974ffed8..ffc7b76299 100644
--- a/gui/src/renderer/containers/ConnectPage.tsx
+++ b/gui/src/renderer/containers/ConnectPage.tsx
@@ -64,10 +64,10 @@ function getRelayName(
}
}
-const mapStateToProps = (state: IReduxState, props: ISharedRouteProps) => {
+const mapStateToProps = (state: IReduxState, _props: ISharedRouteProps) => {
return {
accountExpiry: state.account.expiry
- ? new AccountExpiry(state.account.expiry, props.locale)
+ ? new AccountExpiry(state.account.expiry, state.userInterface.locale)
: undefined,
selectedRelayName: getRelayName(state.settings.relaySettings, state.settings.relayLocations),
connection: state.connection,
diff --git a/gui/src/renderer/containers/SelectLanguagePage.tsx b/gui/src/renderer/containers/SelectLanguagePage.tsx
new file mode 100644
index 0000000000..248e63e367
--- /dev/null
+++ b/gui/src/renderer/containers/SelectLanguagePage.tsx
@@ -0,0 +1,31 @@
+import { goBack } from 'connected-react-router';
+import { connect } from 'react-redux';
+import { bindActionCreators } from 'redux';
+import SelectLanguage from '../components/SelectLanguage';
+
+import { IReduxState, ReduxDispatch } from '../redux/store';
+import { ISharedRouteProps } from '../routes';
+
+const mapStateToProps = (state: IReduxState) => ({
+ preferredLocale: state.settings.guiSettings.preferredLocale,
+});
+
+const mapDispatchToProps = (dispatch: ReduxDispatch, props: ISharedRouteProps) => {
+ const history = bindActionCreators({ goBack }, dispatch);
+
+ return {
+ preferredLocalesList: props.app.getPreferredLocaleList(),
+ setPreferredLocale(locale: string) {
+ props.app.setPreferredLocale(locale);
+ history.goBack();
+ },
+ onClose() {
+ history.goBack();
+ },
+ };
+};
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps,
+)(SelectLanguage);
diff --git a/gui/src/renderer/containers/SettingsPage.tsx b/gui/src/renderer/containers/SettingsPage.tsx
index eb036920c1..927a2cd40f 100644
--- a/gui/src/renderer/containers/SettingsPage.tsx
+++ b/gui/src/renderer/containers/SettingsPage.tsx
@@ -7,10 +7,11 @@ import Settings from '../components/Settings';
import { IReduxState, ReduxDispatch } from '../redux/store';
import { ISharedRouteProps } from '../routes';
-const mapStateToProps = (state: IReduxState, props: ISharedRouteProps) => ({
+const mapStateToProps = (state: IReduxState, _props: ISharedRouteProps) => ({
+ preferredLocaleDisplayName: state.userInterface.preferredLocaleName,
loginState: state.account.status,
accountExpiry: state.account.expiry,
- expiryLocale: props.locale,
+ expiryLocale: state.userInterface.locale,
appVersion: state.version.current,
consistentVersion: state.version.consistent,
upToDateVersion: !state.version.currentIsOutdated,
@@ -21,6 +22,7 @@ const mapDispatchToProps = (dispatch: ReduxDispatch, _props: ISharedRouteProps)
return {
onQuit: () => remote.app.quit(),
onClose: () => history.goBack(),
+ onViewSelectLanguage: () => history.push('/settings/language'),
onViewAccount: () => history.push('/settings/account'),
onViewSupport: () => history.push('/settings/support'),
onViewPreferences: () => history.push('/settings/preferences'),
diff --git a/gui/src/renderer/containers/WireguardKeysPage.tsx b/gui/src/renderer/containers/WireguardKeysPage.tsx
index 813526ff29..8c88a042cc 100644
--- a/gui/src/renderer/containers/WireguardKeysPage.tsx
+++ b/gui/src/renderer/containers/WireguardKeysPage.tsx
@@ -8,10 +8,10 @@ import { IWgKey } from '../redux/settings/reducers';
import { IReduxState, ReduxDispatch } from '../redux/store';
import { ISharedRouteProps } from '../routes';
-const mapStateToProps = (state: IReduxState, props: ISharedRouteProps) => ({
+const mapStateToProps = (state: IReduxState, _props: ISharedRouteProps) => ({
keyState: state.settings.wireguardKeyState,
isOffline: state.connection.isBlocked,
- locale: props.locale,
+ locale: state.userInterface.locale,
});
const mapDispatchToProps = (dispatch: ReduxDispatch, props: ISharedRouteProps) => {
const history = bindActionCreators({ push, goBack }, dispatch);
diff --git a/gui/src/renderer/redux/settings/reducers.ts b/gui/src/renderer/redux/settings/reducers.ts
index fed04b38e4..585eefbbb2 100644
--- a/gui/src/renderer/redux/settings/reducers.ts
+++ b/gui/src/renderer/redux/settings/reducers.ts
@@ -135,6 +135,7 @@ export interface ISettingsReduxState {
const initialState: ISettingsReduxState = {
autoStart: false,
guiSettings: {
+ preferredLocale: 'system',
enableSystemNotifications: true,
autoConnect: true,
monochromaticIcon: false,
diff --git a/gui/src/renderer/redux/userinterface/actions.ts b/gui/src/renderer/redux/userinterface/actions.ts
index f9e2fff444..d9895625a3 100644
--- a/gui/src/renderer/redux/userinterface/actions.ts
+++ b/gui/src/renderer/redux/userinterface/actions.ts
@@ -1,5 +1,15 @@
import { LocationScope } from './reducers';
+export interface IUpdateLocaleAction {
+ type: 'UPDATE_LOCALE';
+ locale: string;
+}
+
+export interface IUpdatePreferredLocaleNameAction {
+ type: 'UPDATE_PREFERRED_LOCALE_NAME';
+ name: string;
+}
+
export interface IUpdateWindowArrowPositionAction {
type: 'UPDATE_WINDOW_ARROW_POSITION';
arrowPosition: number;
@@ -15,10 +25,26 @@ export interface ISetLocationScopeAction {
}
export type UserInterfaceAction =
+ | IUpdateLocaleAction
+ | IUpdatePreferredLocaleNameAction
| IUpdateWindowArrowPositionAction
| IUpdateConnectionInfoOpenAction
| ISetLocationScopeAction;
+function updateLocale(locale: string): IUpdateLocaleAction {
+ return {
+ type: 'UPDATE_LOCALE',
+ locale,
+ };
+}
+
+function updatePreferredLocaleName(name: string): IUpdatePreferredLocaleNameAction {
+ return {
+ type: 'UPDATE_PREFERRED_LOCALE_NAME',
+ name,
+ };
+}
+
function updateWindowArrowPosition(arrowPosition: number): IUpdateWindowArrowPositionAction {
return {
type: 'UPDATE_WINDOW_ARROW_POSITION',
@@ -39,4 +65,10 @@ function setLocationScope(scope: LocationScope): ISetLocationScopeAction {
};
}
-export default { updateWindowArrowPosition, toggleConnectionPanel, setLocationScope };
+export default {
+ updateLocale,
+ updatePreferredLocaleName,
+ updateWindowArrowPosition,
+ toggleConnectionPanel,
+ setLocationScope,
+};
diff --git a/gui/src/renderer/redux/userinterface/reducers.ts b/gui/src/renderer/redux/userinterface/reducers.ts
index 729d5194b6..6afaadde86 100644
--- a/gui/src/renderer/redux/userinterface/reducers.ts
+++ b/gui/src/renderer/redux/userinterface/reducers.ts
@@ -6,12 +6,16 @@ export enum LocationScope {
}
export interface IUserInterfaceReduxState {
+ locale: string;
+ preferredLocaleName: string;
arrowPosition?: number;
connectionPanelVisible: boolean;
locationScope: LocationScope;
}
const initialState: IUserInterfaceReduxState = {
+ locale: 'en',
+ preferredLocaleName: 'English',
connectionPanelVisible: false,
locationScope: LocationScope.relay,
};
@@ -21,6 +25,12 @@ export default function(
action: ReduxAction,
): IUserInterfaceReduxState {
switch (action.type) {
+ case 'UPDATE_LOCALE':
+ return { ...state, locale: action.locale };
+
+ case 'UPDATE_PREFERRED_LOCALE_NAME':
+ return { ...state, preferredLocaleName: action.name };
+
case 'UPDATE_WINDOW_ARROW_POSITION':
return { ...state, arrowPosition: action.arrowPosition };
diff --git a/gui/src/renderer/routes.tsx b/gui/src/renderer/routes.tsx
index 814eeae31e..2f715c472a 100644
--- a/gui/src/renderer/routes.tsx
+++ b/gui/src/renderer/routes.tsx
@@ -9,6 +9,7 @@ import LaunchPage from './containers/LaunchPage';
import LoginPage from './containers/LoginPage';
import PlatformWindowContainer from './containers/PlatformWindowContainer';
import PreferencesPage from './containers/PreferencesPage';
+import SelectLanguagePage from './containers/SelectLanguagePage';
import SelectLocationPage from './containers/SelectLocationPage';
import SettingsPage from './containers/SettingsPage';
import SupportPage from './containers/SupportPage';
@@ -17,7 +18,6 @@ import { getTransitionProps } from './transitions';
export interface ISharedRouteProps {
app: App;
- locale: string;
}
type CustomRouteProps = {
@@ -84,6 +84,7 @@ class AppRoutes extends React.Component<IAppRoutesProps, IAppRoutesState> {
<CustomRoute exact={true} path="/login" component={LoginPage} />
<CustomRoute exact={true} path="/connect" component={ConnectPage} />
<CustomRoute exact={true} path="/settings" component={SettingsPage} />
+ <CustomRoute exact={true} path="/settings/language" component={SelectLanguagePage} />
<CustomRoute exact={true} path="/settings/account" component={AccountPage} />
<CustomRoute exact={true} path="/settings/preferences" component={PreferencesPage} />
<CustomRoute
diff --git a/gui/src/renderer/transitions.ts b/gui/src/renderer/transitions.ts
index 21c9884fb0..23ada748aa 100644
--- a/gui/src/renderer/transitions.ts
+++ b/gui/src/renderer/transitions.ts
@@ -40,6 +40,7 @@ const transitions: ITransitionMap = {
* (null) is used to indicate any route.
*/
const transitionRules = [
+ r('/settings', '/settings/language', transitions.push),
r('/settings', '/settings/account', transitions.push),
r('/settings', '/settings/preferences', transitions.push),
r('/settings', '/settings/advanced', transitions.push),