diff --git a/app/soapbox/actions/settings.js b/app/soapbox/actions/settings.js
index 10005844c..815479b96 100644
--- a/app/soapbox/actions/settings.js
+++ b/app/soapbox/actions/settings.js
@@ -21,8 +21,7 @@ const defaultSettings = ImmutableMap({
deleteModal: true,
defaultPrivacy: 'public',
themeMode: 'light',
- // locale: navigator.language.slice(0, 2) || 'en', // FIXME: Dynamic locales
- locale: 'en',
+ locale: navigator.language.split(/[-_]/)[0] || 'en',
systemFont: false,
dyslexicFont: false,
diff --git a/app/soapbox/actions/streaming.js b/app/soapbox/actions/streaming.js
index d02e18746..ef515798a 100644
--- a/app/soapbox/actions/streaming.js
+++ b/app/soapbox/actions/streaming.js
@@ -9,14 +9,13 @@ import {
import { updateNotificationsQueue, expandNotifications } from './notifications';
import { updateConversations } from './conversations';
import { fetchFilters } from './filters';
-import { getLocale } from '../locales';
-
-const { messages } = getLocale();
+import { getSettings } from 'soapbox/actions/settings';
+import messages from 'soapbox/locales/messages';
export function connectTimelineStream(timelineId, path, pollingRefresh = null, accept = null) {
return connectStream (path, pollingRefresh, (dispatch, getState) => {
- const locale = getState().getIn(['meta', 'locale']);
+ const locale = getSettings(getState()).get('locale');
return {
onConnect() {
@@ -36,7 +35,9 @@ export function connectTimelineStream(timelineId, path, pollingRefresh = null, a
dispatch(deleteFromTimelines(data.payload));
break;
case 'notification':
- dispatch(updateNotificationsQueue(JSON.parse(data.payload), messages, locale, window.location.pathname));
+ messages[locale]().then(messages => {
+ dispatch(updateNotificationsQueue(JSON.parse(data.payload), messages, locale, window.location.pathname));
+ }).catch(() => {});
break;
case 'conversation':
dispatch(updateConversations(JSON.parse(data.payload)));
diff --git a/app/soapbox/components/loading_indicator.js b/app/soapbox/components/loading_indicator.js
index d6a5adb6f..205c84d96 100644
--- a/app/soapbox/components/loading_indicator.js
+++ b/app/soapbox/components/loading_indicator.js
@@ -4,7 +4,7 @@ import { FormattedMessage } from 'react-intl';
const LoadingIndicator = () => (
);
diff --git a/app/soapbox/components/relative_timestamp.js b/app/soapbox/components/relative_timestamp.js
index 7cbc85660..e66c031c1 100644
--- a/app/soapbox/components/relative_timestamp.js
+++ b/app/soapbox/components/relative_timestamp.js
@@ -122,7 +122,7 @@ class RelativeTimestamp extends React.Component {
};
state = {
- now: this.props.intl.now(),
+ now: Date.now(),
};
static defaultProps = {
@@ -139,7 +139,7 @@ class RelativeTimestamp extends React.Component {
componentWillReceiveProps(nextProps) {
if (this.props.timestamp !== nextProps.timestamp) {
- this.setState({ now: this.props.intl.now() });
+ this.setState({ now: Date.now() });
}
}
@@ -166,7 +166,7 @@ class RelativeTimestamp extends React.Component {
const delay = delta < 0 ? Math.max(updateInterval, unitDelay - unitRemainder) : Math.max(updateInterval, unitRemainder);
this._timer = setTimeout(() => {
- this.setState({ now: this.props.intl.now() });
+ this.setState({ now: Date.now() });
}, delay);
}
diff --git a/app/soapbox/containers/compose_container.js b/app/soapbox/containers/compose_container.js
deleted file mode 100644
index 928d12a21..000000000
--- a/app/soapbox/containers/compose_container.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import React from 'react';
-import { Provider } from 'react-redux';
-import PropTypes from 'prop-types';
-import configureStore from '../store/configureStore';
-import { hydrateStore } from '../actions/store';
-import { IntlProvider, addLocaleData } from 'react-intl';
-import { getLocale } from '../locales';
-import Compose from '../features/standalone/compose';
-import initialState from '../initial_state';
-import { fetchCustomEmojis } from '../actions/custom_emojis';
-
-const { localeData, messages } = getLocale();
-addLocaleData(localeData);
-
-const store = configureStore();
-
-if (initialState) {
- store.dispatch(hydrateStore(initialState));
-}
-
-store.dispatch(fetchCustomEmojis());
-
-export default class TimelineContainer extends React.PureComponent {
-
- static propTypes = {
- locale: PropTypes.string.isRequired,
- };
-
- render() {
- const { locale } = this.props;
-
- return (
-
-
-
-
-
- );
- }
-
-}
diff --git a/app/soapbox/containers/media_container.js b/app/soapbox/containers/media_container.js
deleted file mode 100644
index 5c6c248b1..000000000
--- a/app/soapbox/containers/media_container.js
+++ /dev/null
@@ -1,92 +0,0 @@
-import React, { PureComponent, Fragment } from 'react';
-import ReactDOM from 'react-dom';
-import PropTypes from 'prop-types';
-import { IntlProvider, addLocaleData } from 'react-intl';
-import { getLocale } from '../locales';
-import MediaGallery from '../components/media_gallery';
-import Video from '../features/video';
-import Card from '../features/status/components/card';
-import Poll from 'soapbox/components/poll';
-import ModalRoot from '../components/modal_root';
-import MediaModal from '../features/ui/components/media_modal';
-import { List as ImmutableList, fromJS } from 'immutable';
-
-const { localeData, messages } = getLocale();
-addLocaleData(localeData);
-
-const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll };
-
-export default class MediaContainer extends PureComponent {
-
- static propTypes = {
- locale: PropTypes.string.isRequired,
- components: PropTypes.object.isRequired,
- };
-
- state = {
- media: null,
- index: null,
- time: null,
- };
-
- handleOpenMedia = (media, index) => {
- document.body.classList.add('with-modals--active');
- this.setState({ media, index });
- }
-
- handleOpenVideo = (video, time) => {
- const media = ImmutableList([video]);
-
- document.body.classList.add('with-modals--active');
- this.setState({ media, time });
- }
-
- handleCloseMedia = () => {
- document.body.classList.remove('with-modals--active');
- this.setState({ media: null, index: null, time: null });
- }
-
- render() {
- const { locale, components } = this.props;
-
- return (
-
-
- {[].map.call(components, (component, i) => {
- const componentName = component.getAttribute('data-component');
- const Component = MEDIA_COMPONENTS[componentName];
- const { media, card, poll, ...props } = JSON.parse(component.getAttribute('data-props'));
-
- Object.assign(props, {
- ...(media ? { media: fromJS(media) } : {}),
- ...(card ? { card: fromJS(card) } : {}),
- ...(poll ? { poll: fromJS(poll) } : {}),
-
- ...(componentName === 'Video' ? {
- onOpenVideo: this.handleOpenVideo,
- } : {
- onOpenMedia: this.handleOpenMedia,
- }),
- });
-
- return ReactDOM.createPortal(
- ,
- component,
- );
- })}
-
- {this.state.media && (
-
- )}
-
-
-
- );
- }
-
-}
diff --git a/app/soapbox/containers/soapbox.js b/app/soapbox/containers/soapbox.js
index 7c196252a..b0577a535 100644
--- a/app/soapbox/containers/soapbox.js
+++ b/app/soapbox/containers/soapbox.js
@@ -14,8 +14,7 @@ import UI from '../features/ui';
// import Introduction from '../features/introduction';
import { fetchCustomEmojis } from '../actions/custom_emojis';
import { hydrateStore } from '../actions/store';
-import { IntlProvider, addLocaleData } from 'react-intl';
-import { getLocale } from '../locales';
+import { IntlProvider } from 'react-intl';
import initialState from '../initial_state';
import ErrorBoundary from '../components/error_boundary';
import { fetchInstance } from 'soapbox/actions/instance';
@@ -24,6 +23,7 @@ import { fetchMe } from 'soapbox/actions/me';
import PublicLayout from 'soapbox/features/public_layout';
import { getSettings } from 'soapbox/actions/settings';
import { generateThemeCss } from 'soapbox/utils/theme';
+import messages from 'soapbox/locales/messages';
export const store = configureStore();
const hydrateAction = hydrateStore(initialState);
@@ -69,12 +69,35 @@ class SoapboxMount extends React.PureComponent {
dispatch: PropTypes.func,
};
+ state = {
+ messages: {},
+ localeLoading: true,
+ }
+
+ setMessages = () => {
+ messages[this.props.locale]().then(messages => {
+ this.setState({ messages, localeLoading: false });
+ }).catch(() => {});
+ }
+
+ maybeUpdateMessages = prevProps => {
+ if (this.props.locale !== prevProps.locale) {
+ this.setMessages();
+ };
+ }
+
+ componentDidMount() {
+ this.setMessages();
+ }
+
+ componentDidUpdate(prevProps) {
+ this.maybeUpdateMessages(prevProps);
+ }
+
render() {
const { me, themeCss, locale } = this.props;
if (me === null) return null;
-
- const { localeData, messages } = getLocale();
- addLocaleData(localeData);
+ if (this.state.localeLoading) return null;
// Disabling introduction for launch
// const { showIntroduction } = this.props;
@@ -91,7 +114,7 @@ class SoapboxMount extends React.PureComponent {
});
return (
-
+
<>
diff --git a/app/soapbox/containers/timeline_container.js b/app/soapbox/containers/timeline_container.js
deleted file mode 100644
index 550d891d3..000000000
--- a/app/soapbox/containers/timeline_container.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import React, { Fragment } from 'react';
-import ReactDOM from 'react-dom';
-import { Provider } from 'react-redux';
-import PropTypes from 'prop-types';
-import configureStore from '../store/configureStore';
-import { hydrateStore } from '../actions/store';
-import { IntlProvider, addLocaleData } from 'react-intl';
-import { getLocale } from '../locales';
-import PublicTimeline from '../features/standalone/public_timeline';
-import HashtagTimeline from '../features/standalone/hashtag_timeline';
-import ModalContainer from '../features/ui/containers/modal_container';
-import initialState from '../initial_state';
-
-const { localeData, messages } = getLocale();
-addLocaleData(localeData);
-
-const store = configureStore();
-
-if (initialState) {
- store.dispatch(hydrateStore(initialState));
-}
-
-export default class TimelineContainer extends React.PureComponent {
-
- static propTypes = {
- locale: PropTypes.string.isRequired,
- hashtag: PropTypes.string,
- local: PropTypes.bool,
- };
-
- render() {
- const { locale, hashtag, local } = this.props;
-
- let timeline;
-
- if (hashtag) {
- timeline = ;
- } else {
- timeline = ;
- }
-
- return (
-
-
-
- {timeline}
-
- {ReactDOM.createPortal(
- ,
- document.getElementById('modal-container'),
- )}
-
-
-
- );
- }
-
-}
diff --git a/app/soapbox/features/preferences/index.js b/app/soapbox/features/preferences/index.js
index 97225ceee..3530c3530 100644
--- a/app/soapbox/features/preferences/index.js
+++ b/app/soapbox/features/preferences/index.js
@@ -15,6 +15,68 @@ import {
} from 'soapbox/features/forms';
import SettingsCheckbox from './components/settings_checkbox';
+const languages = {
+ en: 'English',
+ ar: 'العربية',
+ ast: 'Asturianu',
+ bg: 'Български',
+ bn: 'বাংলা',
+ ca: 'Català',
+ co: 'Corsu',
+ cs: 'Čeština',
+ cy: 'Cymraeg',
+ da: 'Dansk',
+ de: 'Deutsch',
+ el: 'Ελληνικά',
+ eo: 'Esperanto',
+ es: 'Español',
+ eu: 'Euskara',
+ fa: 'فارسی',
+ fi: 'Suomi',
+ fr: 'Français',
+ ga: 'Gaeilge',
+ gl: 'Galego',
+ he: 'עברית',
+ hi: 'हिन्दी',
+ hr: 'Hrvatski',
+ hu: 'Magyar',
+ hy: 'Հայերեն',
+ id: 'Bahasa Indonesia',
+ io: 'Ido',
+ it: 'Italiano',
+ ja: '日本語',
+ ka: 'ქართული',
+ kk: 'Қазақша',
+ ko: '한국어',
+ lt: 'Lietuvių',
+ lv: 'Latviešu',
+ ml: 'മലയാളം',
+ ms: 'Bahasa Melayu',
+ nl: 'Nederlands',
+ no: 'Norsk',
+ oc: 'Occitan',
+ pl: 'Polski',
+ pt: 'Português',
+ 'pt-BR': 'Português do Brasil',
+ ro: 'Română',
+ ru: 'Русский',
+ sk: 'Slovenčina',
+ sl: 'Slovenščina',
+ sq: 'Shqip',
+ sr: 'Српски',
+ 'sr-Latn': 'Srpski (latinica)',
+ sv: 'Svenska',
+ ta: 'தமிழ்',
+ te: 'తెలుగు',
+ th: 'ไทย',
+ tr: 'Türkçe',
+ uk: 'Українська',
+ zh: '中文',
+ 'zh-CN': '简体中文',
+ 'zh-HK': '繁體中文(香港)',
+ 'zh-TW': '繁體中文(臺灣)',
+};
+
const messages = defineMessages({
heading: { id: 'column.preferences', defaultMessage: 'Preferences' },
});
@@ -33,10 +95,11 @@ class Preferences extends ImmutablePureComponent {
settings: ImmutablePropTypes.map,
};
- onThemeChange = e => {
- const { dispatch } = this.props;
- dispatch(changeSetting(['themeMode'], e.target.value));
- }
+ onSelectChange = path => {
+ return e => {
+ this.props.dispatch(changeSetting(path, e.target.value));
+ };
+ };
onDefaultPrivacyChange = e => {
const { dispatch } = this.props;
@@ -51,10 +114,19 @@ class Preferences extends ImmutablePureComponent {
+
+
+
+
diff --git a/app/soapbox/features/public_layout/index.js b/app/soapbox/features/public_layout/index.js
index afc04b51f..e475d0ac6 100644
--- a/app/soapbox/features/public_layout/index.js
+++ b/app/soapbox/features/public_layout/index.js
@@ -14,7 +14,7 @@ const mapStateToProps = (state, props) => ({
});
const wave = (
-