diff options
26 files changed, 488 insertions, 51 deletions
diff --git a/app/assets/javascripts/components/components/status_action_bar.jsx b/app/assets/javascripts/components/components/status_action_bar.jsx index 8883d0806..051b898bd 100644 --- a/app/assets/javascripts/components/components/status_action_bar.jsx +++ b/app/assets/javascripts/components/components/status_action_bar.jsx @@ -2,7 +2,15 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import PureRenderMixin from 'react-addons-pure-render-mixin'; import IconButton from './icon_button'; import DropdownMenu from './dropdown_menu'; -import { injectIntl } from 'react-intl'; +import { defineMessages, injectIntl } from 'react-intl'; + +const messages = defineMessages({ + delete: { id: 'status.delete', defaultMessage: 'Delete' }, + mention: { id: 'status.mention', defaultMessage: 'Mention' }, + reply: { id: 'status.reply', defaultMessage: 'Reply' }, + reblog: { id: 'status.reblog', defaultMessage: 'Reblog' }, + favourite: { id: 'status.favourite', defaultMessage: 'Favourite' } +}); const StatusActionBar = React.createClass({ propTypes: { @@ -41,16 +49,16 @@ const StatusActionBar = React.createClass({ let menu = []; if (status.getIn(['account', 'id']) === me) { - menu.push({ text: intl.formatMessage({ id: 'status.delete', defaultMessage: 'Delete' }), action: this.handleDeleteClick }); + menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); } else { - menu.push({ text: intl.formatMessage({ id: 'status.mention', defaultMessage: 'Mention' }), action: this.handleMentionClick }); + menu.push({ text: intl.formatMessage(messages.mention), action: this.handleMentionClick }); } return ( <div style={{ marginTop: '10px', overflow: 'hidden' }}> - <div style={{ float: 'left', marginRight: '18px'}}><IconButton title={intl.formatMessage({ id: 'status.reply', defaultMessage: 'Reply' })} icon='reply' onClick={this.handleReplyClick} /></div> - <div style={{ float: 'left', marginRight: '18px'}}><IconButton active={status.get('reblogged')} title={intl.formatMessage({ id: 'status.reblog', defaultMessage: 'Reblog' })} icon='retweet' onClick={this.handleReblogClick} /></div> - <div style={{ float: 'left', marginRight: '18px'}}><IconButton active={status.get('favourited')} title={intl.formatMessage({ id: 'status.favourite', defaultMessage: 'Favourite' })} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div> + <div style={{ float: 'left', marginRight: '18px'}}><IconButton title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReplyClick} /></div> + <div style={{ float: 'left', marginRight: '18px'}}><IconButton active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon='retweet' onClick={this.handleReblogClick} /></div> + <div style={{ float: 'left', marginRight: '18px'}}><IconButton active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div> <div style={{ width: '18px', height: '18px', float: 'left' }}> <DropdownMenu items={menu} icon='ellipsis-h' size={18} /> diff --git a/app/assets/javascripts/components/components/video_player.jsx b/app/assets/javascripts/components/components/video_player.jsx index 2c236b996..9b9b0a2e4 100644 --- a/app/assets/javascripts/components/components/video_player.jsx +++ b/app/assets/javascripts/components/components/video_player.jsx @@ -1,7 +1,11 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import PureRenderMixin from 'react-addons-pure-render-mixin'; import IconButton from './icon_button'; -import { injectIntl } from 'react-intl'; +import { defineMessages, injectIntl } from 'react-intl'; + +const messages = defineMessages({ + toggle_sound: { id: 'video_player.toggle_sound', defaultMessage: 'Toggle sound' } +}); const videoStyle = { position: 'relative', @@ -64,7 +68,7 @@ const VideoPlayer = React.createClass({ return ( <div style={{ cursor: 'default', marginTop: '8px', overflow: 'hidden', width: `${width}px`, height: `${height}px`, boxSizing: 'border-box', background: '#000', position: 'relative' }}> - <div style={muteStyle}><IconButton title={intl.formatMessage({ id: 'video_player.toggle_sound', defaultMessage: 'Toggle sound' })} icon={this.state.muted ? 'volume-up' : 'volume-off'} onClick={this.handleClick} /></div> + <div style={muteStyle}><IconButton title={intl.formatMessage(messages.toggle_sound)} icon={this.state.muted ? 'volume-up' : 'volume-off'} onClick={this.handleClick} /></div> <video src={media.get('url')} autoPlay='true' loop={true} muted={this.state.muted} style={videoStyle} onClick={this.handleVideoClick} /> </div> ); diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx index a12b19746..e61107cd1 100644 --- a/app/assets/javascripts/components/containers/mastodon.jsx +++ b/app/assets/javascripts/components/containers/mastodon.jsx @@ -34,6 +34,8 @@ import Favourites from '../features/favourites'; import HashtagTimeline from '../features/hashtag_timeline'; import { IntlProvider, addLocaleData } from 'react-intl'; import en from 'react-intl/locale-data/en'; +import de from 'react-intl/locale-data/de'; +import getMessagesForLocale from '../locales'; const store = configureStore(); @@ -41,7 +43,7 @@ const browserHistory = useRouterHistory(createBrowserHistory)({ basename: '/web' }); -addLocaleData([...en]); +addLocaleData([...en, ...de]); const Mastodon = React.createClass({ @@ -89,7 +91,7 @@ const Mastodon = React.createClass({ const { locale } = this.props; return ( - <IntlProvider locale={locale}> + <IntlProvider locale={locale} messages={getMessagesForLocale(locale)}> <Provider store={store}> <Router history={browserHistory} render={applyRouterMiddleware(useScroll())}> <Route path='/' component={UI}> diff --git a/app/assets/javascripts/components/features/account/components/action_bar.jsx b/app/assets/javascripts/components/features/account/components/action_bar.jsx index f8d051439..cd01de2e2 100644 --- a/app/assets/javascripts/components/features/account/components/action_bar.jsx +++ b/app/assets/javascripts/components/features/account/components/action_bar.jsx @@ -2,7 +2,17 @@ import PureRenderMixin from 'react-addons-pure-render-mixin'; import ImmutablePropTypes from 'react-immutable-proptypes'; import DropdownMenu from '../../../components/dropdown_menu'; import { Link } from 'react-router'; -import { injectIntl, FormattedMessage, FormattedNumber } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl'; + +const messages = defineMessages({ + mention: { id: 'account.mention', defaultMessage: 'Mention' }, + edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, + unblock: { id: 'account.unblock', defaultMessage: 'Unblock' }, + unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, + block: { id: 'account.block', defaultMessage: 'Block' }, + follow: { id: 'account.follow', defaultMessage: 'Follow' }, + block: { id: 'account.block', defaultMessage: 'Block' } +}); const outerStyle = { borderTop: '1px solid #363c4b', @@ -41,18 +51,18 @@ const ActionBar = React.createClass({ let menu = []; - menu.push({ text: intl.formatMessage({ id: 'account.mention', defaultMessage: 'Mention' }), action: this.props.onMention }); + menu.push({ text: intl.formatMessage(messages.mention), action: this.props.onMention }); if (account.get('id') === me) { - menu.push({ text: intl.formatMessage({ id: 'account.edit_profile', defaultMessage: 'Edit profile' }), href: '/settings/profile' }); + menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' }); } else if (account.getIn(['relationship', 'blocking'])) { - menu.push({ text: intl.formatMessage({ id: 'account.unblock', defaultMessage: 'Unblock' }), action: this.props.onBlock }); + menu.push({ text: intl.formatMessage(messages.unblock), action: this.props.onBlock }); } else if (account.getIn(['relationship', 'following'])) { - menu.push({ text: intl.formatMessage({ id: 'account.unfollow', defaultMessage: 'Unfollow' }), action: this.props.onFollow }); - menu.push({ text: intl.formatMessage({ id: 'account.block', defaultMessage: 'Block' }), action: this.props.onBlock }); + menu.push({ text: intl.formatMessage(messages.unfollow), action: this.props.onFollow }); + menu.push({ text: intl.formatMessage(messages.block), action: this.props.onBlock }); } else { - menu.push({ text: intl.formatMessage({ id: 'account.follow', defaultMessage: 'Follow' }), action: this.props.onFollow }); - menu.push({ text: intl.formatMessage({ id: 'account.block', defaultMessage: 'Block' }), action: this.props.onBlock }); + menu.push({ text: intl.formatMessage(messages.follow), action: this.props.onFollow }); + menu.push({ text: intl.formatMessage(messages.block), action: this.props.onBlock }); } return ( diff --git a/app/assets/javascripts/components/features/account/components/header.jsx b/app/assets/javascripts/components/features/account/components/header.jsx index 7a086c99a..b3e9e2a9f 100644 --- a/app/assets/javascripts/components/features/account/components/header.jsx +++ b/app/assets/javascripts/components/features/account/components/header.jsx @@ -2,6 +2,7 @@ import PureRenderMixin from 'react-addons-pure-render-mixin'; import ImmutablePropTypes from 'react-immutable-proptypes'; import emojify from '../../../emoji'; import escapeTextContentForBrowser from 'react/lib/escapeTextContentForBrowser'; +import { FormattedMessage } from 'react-intl'; const Header = React.createClass({ @@ -23,7 +24,7 @@ const Header = React.createClass({ } if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) { - info = <span style={{ position: 'absolute', top: '10px', right: '10px', opacity: '0.7', display: 'inline-block', verticalAlign: 'top', background: 'rgba(0, 0, 0, 0.4)', color: '#fff', textTransform: 'uppercase', fontSize: '11px', fontWeight: '500', padding: '4px', borderRadius: '4px' }}>Follows you</span> + info = <span style={{ position: 'absolute', top: '10px', right: '10px', opacity: '0.7', display: 'inline-block', verticalAlign: 'top', background: 'rgba(0, 0, 0, 0.4)', color: '#fff', textTransform: 'uppercase', fontSize: '11px', fontWeight: '500', padding: '4px', borderRadius: '4px' }}><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span> } const content = { __html: emojify(account.get('note')) }; diff --git a/app/assets/javascripts/components/features/compose/components/compose_form.jsx b/app/assets/javascripts/components/features/compose/components/compose_form.jsx index 5aa041f09..32bdeaeca 100644 --- a/app/assets/javascripts/components/features/compose/components/compose_form.jsx +++ b/app/assets/javascripts/components/features/compose/components/compose_form.jsx @@ -8,7 +8,12 @@ import Autosuggest from 'react-autosuggest'; import AutosuggestAccountContainer from '../../compose/containers/autosuggest_account_container'; import { debounce } from 'react-decoration'; import UploadButtonContainer from '../containers/upload_button_container'; -import { injectIntl } from 'react-intl'; +import { defineMessages, injectIntl } from 'react-intl'; + +const messages = defineMessages({ + placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' }, + publish: { id: 'compose_form.publish', defaultMessage: 'Publish' } +}); const getTokenForSuggestions = (str, caretPosition) => { let word; @@ -53,7 +58,7 @@ const textareaStyle = { }; const renderInputComponent = inputProps => ( - <textarea {...inputProps} placeholder='What is on your mind?' className='compose-form__textarea' style={textareaStyle} /> + <textarea {...inputProps} className='compose-form__textarea' style={textareaStyle} /> ); const ComposeForm = React.createClass({ @@ -144,7 +149,7 @@ const ComposeForm = React.createClass({ } const inputProps = { - placeholder: intl.formatMessage({ id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' }), + placeholder: intl.formatMessage(messages.placeholder), value: this.props.text, onKeyUp: this.handleKeyUp, onChange: this.handleChange, @@ -169,7 +174,7 @@ const ComposeForm = React.createClass({ /> <div style={{ marginTop: '10px', overflow: 'hidden' }}> - <div style={{ float: 'right' }}><Button text={intl.formatMessage({ id: 'compose_form.publish', defaultMessage: 'Publish' })} onClick={this.handleSubmit} disabled={disabled} /></div> + <div style={{ float: 'right' }}><Button text={intl.formatMessage(messages.publish)} onClick={this.handleSubmit} disabled={disabled} /></div> <div style={{ float: 'right', marginRight: '16px', lineHeight: '36px' }}><CharacterCounter max={500} text={this.props.text} /></div> <UploadButtonContainer style={{ paddingTop: '4px' }} /> </div> diff --git a/app/assets/javascripts/components/features/compose/components/reply_indicator.jsx b/app/assets/javascripts/components/features/compose/components/reply_indicator.jsx index 4b34f09bf..e913ddfa9 100644 --- a/app/assets/javascripts/components/features/compose/components/reply_indicator.jsx +++ b/app/assets/javascripts/components/features/compose/components/reply_indicator.jsx @@ -4,7 +4,11 @@ import Avatar from '../../../components/avatar'; import IconButton from '../../../components/icon_button'; import DisplayName from '../../../components/display_name'; import emojify from '../../../emoji'; -import { injectIntl } from 'react-intl'; +import { defineMessages, injectIntl } from 'react-intl'; + +const messages = defineMessages({ + cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' } +}); const ReplyIndicator = React.createClass({ @@ -37,7 +41,7 @@ const ReplyIndicator = React.createClass({ return ( <div style={{ background: '#9baec8', padding: '10px' }}> <div style={{ overflow: 'hidden', marginBottom: '5px' }}> - <div style={{ float: 'right', lineHeight: '24px' }}><IconButton title={intl.formatMessage({ id: 'reply_indicator.cancel', defaultMessage: 'Cancel' })} icon='times' onClick={this.handleClick} /></div> + <div style={{ float: 'right', lineHeight: '24px' }}><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} /></div> <a href={this.props.status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px', color: '#282c37', textDecoration: 'none', overflow: 'hidden', lineHeight: '24px' }}> <div style={{ float: 'left', marginRight: '5px' }}><Avatar size={24} src={this.props.status.getIn(['account', 'avatar'])} /></div> diff --git a/app/assets/javascripts/components/features/compose/components/search.jsx b/app/assets/javascripts/components/features/compose/components/search.jsx index 65df336cc..b4e618820 100644 --- a/app/assets/javascripts/components/features/compose/components/search.jsx +++ b/app/assets/javascripts/components/features/compose/components/search.jsx @@ -3,7 +3,11 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import Autosuggest from 'react-autosuggest'; import AutosuggestAccountContainer from '../containers/autosuggest_account_container'; import { debounce } from 'react-decoration'; -import { injectIntl } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + +const messages = defineMessages({ + placeholder: { id: 'search.placeholder', defaultMessage: 'Search' } +}); const getSuggestionValue = suggestion => suggestion.value; @@ -16,7 +20,7 @@ const renderSuggestion = suggestion => { }; const renderSectionTitle = section => ( - <strong>{section.title}</strong> + <strong><FormattedMessage id={`search.${section.title}`} defaultMessage={section.title} /></strong> ); const getSectionSuggestions = section => section.items; @@ -95,7 +99,7 @@ const Search = React.createClass({ render () { const inputProps = { - placeholder: this.props.intl.formatMessage({ id: 'search.placeholder', defaultMessage: 'Search' }), + placeholder: this.props.intl.formatMessage(messages.placeholder), value: this.props.value, onChange: this.onChange, style: inputStyle diff --git a/app/assets/javascripts/components/features/compose/components/upload_button.jsx b/app/assets/javascripts/components/features/compose/components/upload_button.jsx index cc251835f..5250ff748 100644 --- a/app/assets/javascripts/components/features/compose/components/upload_button.jsx +++ b/app/assets/javascripts/components/features/compose/components/upload_button.jsx @@ -1,6 +1,10 @@ import PureRenderMixin from 'react-addons-pure-render-mixin'; import IconButton from '../../../components/icon_button'; -import { injectIntl } from 'react-intl'; +import { defineMessages, injectIntl } from 'react-intl'; + +const messages = defineMessages({ + upload: { id: 'upload_button.label', defaultMessage: 'Add media' } +}); const UploadButton = React.createClass({ @@ -31,7 +35,7 @@ const UploadButton = React.createClass({ return ( <div style={this.props.style}> - <IconButton icon='photo' title={intl.formatMessage({ id: 'upload_button.label', defaultMessage: 'Add media' })} disabled={this.props.disabled} onClick={this.handleClick} size={24} /> + <IconButton icon='photo' title={intl.formatMessage(messages.upload)} disabled={this.props.disabled} onClick={this.handleClick} size={24} /> <input ref={this.setRef} type='file' multiple={false} onChange={this.handleChange} disabled={this.props.disabled} style={{ display: 'none' }} /> </div> ); diff --git a/app/assets/javascripts/components/features/compose/components/upload_form.jsx b/app/assets/javascripts/components/features/compose/components/upload_form.jsx index 72c2b9535..ac548033c 100644 --- a/app/assets/javascripts/components/features/compose/components/upload_form.jsx +++ b/app/assets/javascripts/components/features/compose/components/upload_form.jsx @@ -1,7 +1,11 @@ import PureRenderMixin from 'react-addons-pure-render-mixin'; import ImmutablePropTypes from 'react-immutable-proptypes'; import IconButton from '../../../components/icon_button'; -import { injectIntl } from 'react-intl'; +import { defineMessages, injectIntl } from 'react-intl'; + +const messages = defineMessages({ + undo: { id: 'upload_form.undo', defaultMessage: 'Undo' } +}); const UploadForm = React.createClass({ @@ -19,7 +23,7 @@ const UploadForm = React.createClass({ const uploads = this.props.media.map(attachment => ( <div key={attachment.get('id')} style={{ borderRadius: '4px', marginBottom: '10px' }} className='transparent-background'> <div style={{ width: '100%', height: '100px', borderRadius: '4px', background: `url(${attachment.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }}> - <IconButton icon='times' title={intl.formatMessage({ id: 'upload_form.undo', defaultMessage: 'Undo' })} size={36} onClick={this.props.onRemoveFile.bind(this, attachment.get('id'))} /> + <IconButton icon='times' title={intl.formatMessage(messages.undo)} size={36} onClick={this.props.onRemoveFile.bind(this, attachment.get('id'))} /> </div> </div> )); diff --git a/app/assets/javascripts/components/features/followers/components/account.jsx b/app/assets/javascripts/components/features/followers/components/account.jsx index 123a40cab..4a1fca6da 100644 --- a/app/assets/javascripts/components/features/followers/components/account.jsx +++ b/app/assets/javascripts/components/features/followers/components/account.jsx @@ -4,7 +4,11 @@ import Avatar from '../../../components/avatar'; import DisplayName from '../../../components/display_name'; import { Link } from 'react-router'; import IconButton from '../../../components/icon_button'; -import { injectIntl } from 'react-intl'; +import { defineMessages, injectIntl } from 'react-intl'; + +const messages = defineMessages({ + follow: { id: 'account.follow', defaultMessage: 'Follow' } +}); const outerStyle = { padding: '10px', @@ -69,7 +73,7 @@ const Account = React.createClass({ buttons = ( <div style={buttonsStyle}> - <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage({ id: 'account.follow', defaultMessage: 'Follow' })} onClick={this.handleFollow} active={following} /> + <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(messages.follow)} onClick={this.handleFollow} active={following} /> </div> ); } diff --git a/app/assets/javascripts/components/features/home_timeline/index.jsx b/app/assets/javascripts/components/features/home_timeline/index.jsx index 117a4a72d..e4f4fa7c7 100644 --- a/app/assets/javascripts/components/features/home_timeline/index.jsx +++ b/app/assets/javascripts/components/features/home_timeline/index.jsx @@ -3,7 +3,11 @@ import PureRenderMixin from 'react-addons-pure-render-mixin'; import StatusListContainer from '../ui/containers/status_list_container'; import Column from '../ui/components/column'; import { refreshTimeline } from '../../actions/timelines'; -import { injectIntl } from 'react-intl'; +import { defineMessages, injectIntl } from 'react-intl'; + +const messages = defineMessages({ + title: { id: 'column.home', defaultMessage: 'Home' } +}); const HomeTimeline = React.createClass({ @@ -21,7 +25,7 @@ const HomeTimeline = React.createClass({ const { intl } = this.props; return ( - <Column icon='home' heading={intl.formatMessage({ id: 'column.home', defaultMessage: 'Home' })}> + <Column icon='home' heading={intl.formatMessage(messages.title)}> <StatusListContainer {...this.props} type='home' /> </Column> ); diff --git a/app/assets/javascripts/components/features/mentions_timeline/index.jsx b/app/assets/javascripts/components/features/mentions_timeline/index.jsx index 9f1caa235..8583f59a6 100644 --- a/app/assets/javascripts/components/features/mentions_timeline/index.jsx +++ b/app/assets/javascripts/components/features/mentions_timeline/index.jsx @@ -3,7 +3,11 @@ import PureRenderMixin from 'react-addons-pure-render-mixin'; import StatusListContainer from '../ui/containers/status_list_container'; import Column from '../ui/components/column'; import { refreshTimeline } from '../../actions/timelines'; -import { injectIntl } from 'react-intl'; +import { defineMessages, injectIntl } from 'react-intl'; + +const messages = defineMessages({ + title: { id: 'column.mentions', defaultMessage: 'Mentions' } +}); const MentionsTimeline = React.createClass({ @@ -21,7 +25,7 @@ const MentionsTimeline = React.createClass({ const { intl } = this.props; return ( - <Column icon='at' heading={intl.formatMessage({ id: 'column.mentions', defaultMessage: 'Mentions' })}> + <Column icon='at' heading={intl.formatMessage(messages.title)}> <StatusListContainer {...this.props} type='mentions' /> </Column> ); diff --git a/app/assets/javascripts/components/features/public_timeline/index.jsx b/app/assets/javascripts/components/features/public_timeline/index.jsx index 445a4fc63..c3da09a09 100644 --- a/app/assets/javascripts/components/features/public_timeline/index.jsx +++ b/app/assets/javascripts/components/features/public_timeline/index.jsx @@ -7,7 +7,11 @@ import { updateTimeline, deleteFromTimelines } from '../../actions/timelines'; -import { injectIntl } from 'react-intl'; +import { defineMessages, injectIntl } from 'react-intl'; + +const messages = defineMessages({ + title: { id: 'column.public', defaultMessage: 'Public' } +}); const PublicTimeline = React.createClass({ @@ -48,7 +52,7 @@ const PublicTimeline = React.createClass({ const { intl } = this.props; return ( - <Column icon='globe' heading={intl.formatMessage({ id: 'column.public', defaultMessage: 'Public' })}> + <Column icon='globe' heading={intl.formatMessage(messages.title)}> <StatusListContainer type='public' /> </Column> ); diff --git a/app/assets/javascripts/components/features/status/components/action_bar.jsx b/app/assets/javascripts/components/features/status/components/action_bar.jsx index d855176f2..1f46b956e 100644 --- a/app/assets/javascripts/components/features/status/components/action_bar.jsx +++ b/app/assets/javascripts/components/features/status/components/action_bar.jsx @@ -2,7 +2,15 @@ import PureRenderMixin from 'react-addons-pure-render-mixin'; import IconButton from '../../../components/icon_button'; import ImmutablePropTypes from 'react-immutable-proptypes'; import DropdownMenu from '../../../components/dropdown_menu'; -import { injectIntl } from 'react-intl'; +import { defineMessages, injectIntl } from 'react-intl'; + +const messages = defineMessages({ + delete: { id: 'status.delete', defaultMessage: 'Delete' }, + mention: { id: 'status.mention', defaultMessage: 'Mention' }, + reply: { id: 'status.reply', defaultMessage: 'Reply' }, + reblog: { id: 'status.reblog', defaultMessage: 'Reblog' }, + favourite: { id: 'status.favourite', defaultMessage: 'Favourite' } +}); const ActionBar = React.createClass({ @@ -44,16 +52,16 @@ const ActionBar = React.createClass({ let menu = []; if (me === status.getIn(['account', 'id'])) { - menu.push({ text: intl.formatMessage({ id: 'status.delete', defaultMessage: 'Delete' }), action: this.handleDeleteClick }); + menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); } else { - menu.push({ text: intl.formatMessage({ id: 'status.mention', defaultMessage: 'Mention' }), action: this.handleMentionClick }); + menu.push({ text: intl.formatMessage(messages.mention), action: this.handleMentionClick }); } return ( <div style={{ background: '#2f3441', display: 'flex', flexDirection: 'row', borderTop: '1px solid #363c4b', borderBottom: '1px solid #363c4b', padding: '10px 0' }}> - <div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton title={intl.formatMessage({ id: 'status.reply', defaultMessage: 'Reply' })} icon='reply' onClick={this.handleReplyClick} /></div> - <div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton active={status.get('reblogged')} title={intl.formatMessage({ id: 'status.reblog', defaultMessage: 'Reblog' })} icon='retweet' onClick={this.handleReblogClick} /></div> - <div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton active={status.get('favourited')} title={intl.formatMessage({ id: 'status.favourite', defaultMessage: 'Favourite' })} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div> + <div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReplyClick} /></div> + <div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon='retweet' onClick={this.handleReblogClick} /></div> + <div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div> <div style={{ flex: '1 1 auto', textAlign: 'center' }}><DropdownMenu size={18} icon='ellipsis-h' items={menu} /></div> </div> ); diff --git a/app/assets/javascripts/components/locales/de.jsx b/app/assets/javascripts/components/locales/de.jsx new file mode 100644 index 000000000..e6f4a2491 --- /dev/null +++ b/app/assets/javascripts/components/locales/de.jsx @@ -0,0 +1,48 @@ +const en = { + "column_back_button.label": "Zurück", + "lightbox.close": "Schließen", + "loading_indicator.label": "Lade...", + "status.mention": "Erwähnen", + "status.delete": "Löschen", + "status.reply": "Antworten", + "status.reblog": "Teilen", + "status.favourite": "Favorisieren", + "status.reblogged_by": "{name} teilte", + "video_player.toggle_sound": "Ton umschalten", + "account.mention": "Erwähnen", + "account.edit_profile": "Profil bearbeiten", + "account.unblock": "Entblocken", + "account.unfollow": "Entfolgen", + "account.block": "Blocken", + "account.follow": "Folgen", + "account.posts": "Beiträge", + "account.follows": "Folgt", + "account.followers": "Folger", + "account.follows_you": "Folgt dir", + "getting_started.heading": "Erste Schritte", + "getting_started.about_addressing": "Du kannst Leuten folgen, falls du ihren Nutzernamen und ihre Domain kennst, in dem du eine e-mail-artige Addresse in das Suchfeld oben an der Seite eingibst.", + "getting_started.about_shortcuts": "Falls der Zielnutzer an derselben Domain ist wie du, funktioniert der Nutzername auch alleine. Das gilt auch für Erwähnungen in Beiträgen.", + "getting_started.about_developer": "Der Entwickler des Projekts kann unter Gargron@mastodon.social gefunden werden", + "column.home": "Home", + "column.mentions": "Erwähnungen", + "column.public": "Gesamtes Bekanntes Netz", + "tabs_bar.compose": "Schreiben", + "tabs_bar.home": "Home", + "tabs_bar.mentions": "Erwähnungen", + "tabs_bar.public": "Gesamtes Netz", + "compose_form.placeholder": "Worüber möchstest du schreiben?", + "compose_form.publish": "Veröffentlichen", + "navigation_bar.settings": "Einstellungen", + "navigation_bar.public_timeline": "Öffentlich", + "navigation_bar.logout": "Abmelden", + "reply_indicator.cancel": "Abbrechen", + "search.placeholder": "Suche", + "search.account": "Konto", + "search.hashtag": "Hashtag", + "suggestions_box.who_to_follow": "Wem folgen", + "suggestions_box.refresh": "Aktualisieren", + "upload_button.label": "Media-Datei anfügen", + "upload_form.undo": "Entfernen" +}; + +export default en; diff --git a/app/assets/javascripts/components/locales/en.jsx b/app/assets/javascripts/components/locales/en.jsx new file mode 100644 index 000000000..a28c84b03 --- /dev/null +++ b/app/assets/javascripts/components/locales/en.jsx @@ -0,0 +1,49 @@ +const en = { + "column_back_button.label": "Back", + "lightbox.close": "Close", + "loading_indicator.label": "Loading...", + "status.mention": "Mention", + "status.delete": "Delete", + "status.reply": "Reply", + "status.reblog": "Reblog", + "status.favourite": "Favourite", + "status.reblogged_by": "{name} reblogged", + "video_player.toggle_sound": "Toggle sound", + "account.mention": "Mention", + "account.edit_profile": "Edit profile", + "account.unblock": "Unblock", + "account.unfollow": "Unfollow", + "account.block": "Block", + "account.follow": "Follow", + "account.block": "Block", + "account.posts": "Posts", + "account.follows": "Follows", + "account.followers": "Followers", + "account.follows_you": "Follows you", + "getting_started.heading": "Getting started", + "getting_started.about_addressing": "You can follow people if you know their username and the domain they are on by entering an e-mail-esque address into the form at the top of the sidebar.", + "getting_started.about_shortcuts": "If the target user is on the same domain as you, just the username will work. The same rule applies to mentioning people in statuses.", + "getting_started.about_developer": "The developer of this project can be followed as Gargron@mastodon.social", + "column.home": "Home", + "column.mentions": "Mentions", + "column.public": "Public", + "tabs_bar.compose": "Compose", + "tabs_bar.home": "Home", + "tabs_bar.mentions": "Mentions", + "tabs_bar.public": "Public", + "compose_form.placeholder": "What is on your mind?", + "compose_form.publish": "Publish", + "navigation_bar.settings": "Settings", + "navigation_bar.public_timeline": "Public timeline", + "navigation_bar.logout": "Logout", + "reply_indicator.cancel": "Cancel", + "search.placeholder": "Search", + "search.account": "Account", + "search.hashtag": "Hashtag", + "suggestions_box.who_to_follow": "Who to follow", + "suggestions_box.refresh": "Refresh", + "upload_button.label": "Add media", + "upload_form.undo": "Undo" +}; + +export default en; diff --git a/app/assets/javascripts/components/locales/index.jsx b/app/assets/javascripts/components/locales/index.jsx new file mode 100644 index 000000000..212cbcee5 --- /dev/null +++ b/app/assets/javascripts/components/locales/index.jsx @@ -0,0 +1,11 @@ +import en from './en'; +import de from './de'; + +const locales = { + en, + de +}; + +export default function getMessagesForLocale (locale) { + return locales[locale]; +}; diff --git a/app/assets/javascripts/components/reducers/search.jsx b/app/assets/javascripts/components/reducers/search.jsx index f3ee17f60..9c2041863 100644 --- a/app/assets/javascripts/components/reducers/search.jsx +++ b/app/assets/javascripts/components/reducers/search.jsx @@ -14,7 +14,7 @@ const initialState = Immutable.Map({ const normalizeSuggestions = (state, value, accounts) => { let newSuggestions = [ { - title: 'Account', + title: 'account', items: accounts.map(item => ({ type: 'account', id: item.id, @@ -25,7 +25,7 @@ const normalizeSuggestions = (state, value, accounts) => { if (value.indexOf('@') === -1) { newSuggestions.push({ - title: 'Hashtag', + title: 'hashtag', items: [ { type: 'hashtag', diff --git a/config/application.rb b/config/application.rb index e992c2481..114de57fb 100644 --- a/config/application.rb +++ b/config/application.rb @@ -20,7 +20,7 @@ module Mastodon # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] - config.i18n.available_locales = [:en] + config.i18n.available_locales = [:en, :de] config.i18n.default_locale = :en # config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb') diff --git a/config/locales/de.yml b/config/locales/de.yml new file mode 100644 index 000000000..648be5db2 --- /dev/null +++ b/config/locales/de.yml @@ -0,0 +1,59 @@ +--- +de: + about: + about_instance: "<em>%{instance}</em> ist eine Instanz von Mastodon." + about_mastodon: Mastodon ist ein <em>freier, quelloffener</em> soziales Netzwerkserver. Eine <em>dezentralisierte</em> Alternative zu kommerziellen Plattformen, verhindert es die Risiken, die entstehen, wenn eine einzelne Firma deine Kommunikation monopolisiert. Jeder kann Mastodon verwenden und ganz einfach am <em>sozialen Netzwerk</em> teilnehmen. + get_started: Erste Schritte + source_code: Quellcode + terms: AGB + accounts: + follow: Folgen + followers: Folger + following: Folgt + nothing_here: Hier gibt es nichts! + people_followed_by: Nutzer, denen %{name} folgt + people_who_follow: Nutzer, die %{name} folgen + posts: Beiträge + unfollow: Entfolgen + application_mailer: + signature: Mastodon-Benachrichtigungen von %{instance} + auth: + change_password: Passwort ändern + didnt_get_confirmation: Keine Bestätigung bekommen? + forgot_password: Passwort vergessen? + login: Anmelden + register: Registrieren + resend_confirmation: Bestätigung nochmal versenden + reset_password: Passwort zurücksetzen + set_new_password: Neues Passwort setzen + generic: + changes_saved_msg: Änderungen gespeichert! + powered_by: angetrieben von %{link} + save_changes: Änderungen speichern + validation_errors: + one: Etwas ist noch nicht ganz richtig! Bitte korrigiere den Fehler + other: Etwas ist noch nicht ganz richtig! Bitte korrigiere %{count} Fehler + notification_mailer: + favourite: + body: "Dein Beitrag wurde von %{name} favorisiert:" + subject: "%{name} hat deinen Beitrag favorisiert" + follow: + body: "%{name} folgt dir jetzt!" + subject: "%{name} folgt dir nun" + mention: + body: "%{name} hat dich erwähnt:" + subject: "%{name} hat dich erwähnt" + reblog: + body: "Dein Beitrag wurde von %{name} geteilt:" + subject: "%{name} teilte deinen Beitrag" + pagination: + next: Vorwärts + prev: Zurück + settings: + edit_profile: Profil bearbeiten + preferences: Einstellungen + stream_entries: + favourited: favorisierte einen Beitrag von + is_now_following: folgt nun + will_paginate: + page_gap: "…" diff --git a/config/locales/devise.de.yml b/config/locales/devise.de.yml new file mode 100644 index 000000000..181502f9c --- /dev/null +++ b/config/locales/devise.de.yml @@ -0,0 +1,61 @@ +--- +de: + devise: + confirmations: + confirmed: "Vielen Dank für Deine Registrierung. Bitte melde dich jetzt an." + send_instructions: "Du erhältst in wenigen Minuten eine E-Mail, mit der Du Deine Registrierung bestätigen kannst." + send_paranoid_instructions: "Falls Deine E-Mail-Adresse in unserer Datenbank existiert erhältst Du in wenigen Minuten eine E-Mail mit der Du Deine Registrierung bestätigen kannst." + failure: + already_authenticated: "Du bist bereits angemeldet." + inactive: "Dein Account ist nicht aktiv." + invalid: "Ungültige Anmeldedaten." + last_attempt: "Du hast noch einen Versuch bevor dein Account gesperrt wird" + locked: "Dein Account ist gesperrt." + not_found_in_database: "E-Mail-Adresse oder Passwort ungültig." + timeout: "Deine Sitzung ist abgelaufen, bitte melde Dich erneut an." + unauthenticated: "Du musst Dich anmelden oder registrieren, bevor Du fortfahren kannst." + unconfirmed: "Du musst Deinen Account bestätigen, bevor Du fortfahren kannst." + mailer: + confirmation_instructions: + subject: "Mastodon: Anleitung zur Bestätigung Deines Accounts" + password_change: + subject: 'Mastodon: Passwort wurde geändert' + reset_password_instructions: + subject: "Mastodon: Anleitung um Dein Passwort zurückzusetzen" + unlock_instructions: + subject: "Mastodon: Anleitung um Deinen Account freizuschalten" + omniauth_callbacks: + failure: "Du konntest nicht Deinem %{kind}-Account angemeldet werden, weil '%{reason}'." + success: "Du hast Dich erfolgreich mit Deinem %{kind}-Account angemeldet." + passwords: + no_token: "Du kannst diese Seite nur von dem Link aus einer E-Mail zum Passwort-Zurücksetzen aufrufen. Wenn du einen solchen Link aufgerufen hast stelle bitte sicher, dass du die vollständige Adresse aufrufst." + send_instructions: "Du erhältst in wenigen Minuten eine E-Mail mit der Anleitung, wie Du Dein Passwort zurücksetzen kannst." + send_paranoid_instructions: "Falls Deine E-Mail-Adresse in unserer Datenbank existiert erhältst Du in wenigen Minuten eine E-Mail mit der Anleitung, wie Du Dein Passwort zurücksetzen können." + updated: "Dein Passwort wurde geändert. Du bist jetzt angemeldet." + updated_not_active: "Dein Passwort wurde geändert." + registrations: + destroyed: "Dein Account wurde gelöscht." + signed_up: "Du hast dich erfolgreich registriert." + signed_up_but_inactive: "Du hast dich erfolgreich registriert. Wir konnten Dich noch nicht anmelden, da Dein Account inaktiv ist." + signed_up_but_locked: "Du hast dich erfolgreich registriert. Wir konnten Dich noch nicht anmelden, da Dein Account gesperrt ist." + signed_up_but_unconfirmed: "Du hast Dich erfolgreich registriert. Wir konnten Dich noch nicht anmelden, da Dein Account noch nicht bestätigt ist. Du erhältst in Kürze eine E-Mail mit der Anleitung, wie Du Deinen Account freischalten kannst." + update_needs_confirmation: "Deine Daten wurden aktualisiert, aber Du musst Deine neue E-Mail-Adresse bestätigen. Du erhälst in wenigen Minuten eine E-Mail, mit der Du die Änderung Deiner E-Mail-Adresse abschließen kannst." + updated: "Deine Daten wurden aktualisiert." + sessions: + already_signed_out: "Erfolgreich abgemeldet." + signed_in: "Erfolgreich angemeldet." + signed_out: "Erfolgreich abgemeldet." + unlocks: + send_instructions: "Du erhältst in wenigen Minuten eine E-Mail mit der Anleitung, wie Du Deinen Account entsperren können." + send_paranoid_instructions: "Falls Deine E-Mail-Adresse in unserer Datenbank existiert erhältst Du in wenigen Minuten eine E-Mail mit der Anleitung, wie Du Deinen Account entsperren kannst." + unlocked: "Dein Account wurde entsperrt. Du bist jetzt angemeldet." + errors: + messages: + already_confirmed: "wurde bereits bestätigt" + confirmation_period_expired: "muss innerhalb %{period} bestätigt werden, bitte fordere einen neuen Link an" + expired: "ist abgelaufen, bitte neu anfordern" + not_found: "nicht gefunden" + not_locked: "ist nicht gesperrt" + not_saved: + one: "Konnte %{resource} nicht speichern: ein Fehler." + other: "Konnte %{resource} nicht speichern: %{count} Fehler." diff --git a/config/locales/doorkeeper.de.yml b/config/locales/doorkeeper.de.yml new file mode 100644 index 000000000..0c606f6a2 --- /dev/null +++ b/config/locales/doorkeeper.de.yml @@ -0,0 +1,112 @@ +--- +de: + activerecord: + attributes: + doorkeeper/application: + name: Name + redirect_uri: Redirect URI + errors: + models: + doorkeeper/application: + attributes: + redirect_uri: + fragment_present: darf kein Fragment enthalten. + invalid_uri: muss ein valider URI (Identifier) sein. + relative_uri: muss ein absoluter URI (Identifier) sein. + secured_uri: muss ein HTTPS/SSL URI (Identifier) sein. + doorkeeper: + applications: + buttons: + authorize: Autorisieren + cancel: Abbrechen + destroy: Löschen + edit: Bearbeiten + submit: Übertragen + confirmations: + destroy: Bist du sicher? + edit: + title: Applikation bearbeiten + form: + error: Whoops! Bitte überprüfe das Formular auf Fehler! + help: + native_redirect_uri: "%{native_redirect_uri} für lokale Tests benutzen" + redirect_uri: Bitte benutze eine Zeile pro URI + scopes: Bitte die "Scopes" mit Leerzeichen trennen. Bitte frei lassen für die Verwendung der Default-Werte. + index: + callback_url: Callback URL + name: Name + new: Neue Applikation + title: Deine Applikationen + new: + title: Neue Applikation + show: + actions: Aktionen + application_id: Applikations-ID + callback_urls: Callback URLs + scopes: Scopes + secret: Secret + title: 'Applikation: %{name}' + authorizations: + buttons: + authorize: Autorisieren + deny: Verweigern + error: + title: Ein Fehler ist aufgetreten + new: + able_to: 'Diese Anwendung wird folgende Rechte haben:' + prompt: Soll %{client_name} für die Benutzung dieses Accounts autorisiert werden? + title: Autorisierung erforderlich + show: + title: Autorisierungscode + authorized_applications: + buttons: + revoke: Ungültig machen + confirmations: + revoke: Bist du sicher? + index: + application: Applikation + created_at: erstellt am + date_format: "%Y-%m-%d %H:%M:%S" + title: Deine autorisierten Applikationen + errors: + messages: + access_denied: Der Resource Owner oder der Autorisierungs-Server hat die Anfrage verweigert. + credential_flow_not_configured: 'Die Prozedur "Resource Owner Password Credentials" ist fehlgeschlagen: Doorkeeper.configure.resource_owner_from_credentials ist nicht konfiguriert.' + invalid_client: 'Client-Autorisierung MKIM ist fehlgeschlagen: Unbekannter Client, keine Autorisierung mitgeliefert oder Autorisierungsmethode nicht unterstützt.' + invalid_grant: Die bereitgestellte Autorisierung ist inkorrekt, abgelaufen, widerrufen, ist mit einem anderen Client verknüpft oder der Redirection URI stimmt nicht mit der Autorisierungs-Anfrage überein. + invalid_redirect_uri: Der Redirect-URI in der Anfrage ist ungültig. + invalid_request: Die Anfrage enthält einen nicht-unterstützten Parameter, ein Parameter fehlt oder sie ist anderweitig fehlerhaft. + invalid_resource_owner: Die angegebenen Zugangsdaten für den "Resource Owner" sind inkorrekt oder dieser Benutzer existiert nicht. + invalid_scope: Der angeforderte Scope ist inkorrekt, unbekannt oder fehlerhaft. + invalid_token: + expired: Der Access Token ist abgelaufen + revoked: Der Access Token wurde annuliert + unknown: Der Access Token ist nicht gültig/korrekt + resource_owner_authenticator_not_configured: 'Die Prozedur "Resource Owner find" ist fehlgeschlagen: Doorkeeper.configure.resource_owner_authenticator ist nicht konfiguriert.' + server_error: Der Autorisierungs-Server hat ein unerwartetes Problem festgestellt und konnte die Anfrage nicht beenden. + temporarily_unavailable: Der Autorisierungs-Server ist derzeit auf Grund von temporärer Überlastung oder Wartungsarbeiten am Server nicht in der Lage, die Anfrage zu bearbeiten . + unauthorized_client: Der Client ist nicht autorisiert, diese Anfrage mit dieser Methode auszuführen. + unsupported_grant_type: Der Autorisierungs-Typ wird nicht vom Autorisierungs-Server unterstützt. + unsupported_response_type: Der Autorisierungs-Server unterstützt diesen Antwort-Typ nicht. + flash: + applications: + create: + notice: Applikation erstellt. + destroy: + notice: Applikation gelöscht. + update: + notice: Applikation geupdated. + authorized_applications: + destroy: + notice: Applikation widerrufen. + layouts: + admin: + nav: + applications: Applikationen + oauth2_provider: OAuth2 Provider + application: + title: OAuth Autorisierung nötig + scopes: + follow: Nutzer folgen, blocken, entblocken und entfolgen + read: deine Daten lesen + write: Beiträge von deinem Konto aus veröffentlichen diff --git a/config/locales/en.yml b/config/locales/en.yml index ab16ed082..426f3928a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -55,3 +55,5 @@ en: stream_entries: favourited: favourited a post by is_now_following: is now following + will_paginate: + page_gap: "…" diff --git a/config/locales/simple_form.de.yml b/config/locales/simple_form.de.yml new file mode 100644 index 000000000..6e6758df2 --- /dev/null +++ b/config/locales/simple_form.de.yml @@ -0,0 +1,27 @@ +--- +de: + simple_form: + labels: + defaults: + avatar: Avatar + confirm_new_password: Neues Passwort bestätigen + confirm_password: Passwort bestätigen + current_password: Derzeitiges Passwort + display_name: Anzeigename + email: E-mail-Addresse + header: Kopfbild + locale: Sprache + new_password: Neues Passwort + note: Über mich + password: Passwort + username: Nutzername + notification_emails: + favourite: E-mail senden, wenn jemand meinen Beitrag favorisiert + follow: E-mail senden, wenn mir jemand folgt + mention: E-mail senden, wenn mich jemand erwähnt + reblog: E-mail senden, wenn jemand meinen Beitrag teilt + 'no': 'Nein' + required: + mark: "*" + text: Pflichtfeld + 'yes': 'Ja' diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index bd22a1f3d..b8a69a075 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -1,8 +1,6 @@ --- en: simple_form: - error_notification: - default_message: 'Please review the problems below:' labels: defaults: avatar: Avatar |