Commit 9c161dbe authored by valzav's avatar valzav
Browse files

merge i18n branch

parent 637df39a
import React from 'react';
import isString from 'lodash/isString';
import isObject from 'lodash/isObject';
import isUndefined from 'lodash/isUndefined';
import { connect } from 'react-redux'
import { IntlProvider, addLocaleData, injectIntl } from 'react-intl';
import store from 'store';
import { DEFAULT_LANGUAGE } from 'app/client_config';
import {connect} from 'react-redux'
import {IntlProvider} from 'react-intl';
import {DEFAULT_LANGUAGE} from 'app/client_config';
import tt from 'counterpart';
// most of this code creates a wrapper for i18n API.
// this is needed to make i18n future proof
tt.registerTranslations('en', require('app/locales/en.json'));
/*
module exports bunch of functions: translate, translateHtml and so on
usage example:
translate('reply_to_user', {username: 'undeadlol1') == 'Reply to undeadlol1'
translateHtml works the same, expcept it renders string with html tags in it
*/
// locale data is needed for various messages, ie 'N minutes ago'
import enLocaleData from 'react-intl/locale-data/en';
import ruLocaleData from 'react-intl/locale-data/ru';
import frLocaleData from 'react-intl/locale-data/fr';
import esLocaleData from 'react-intl/locale-data/es';
import itLocaleData from 'react-intl/locale-data/it';
addLocaleData([...enLocaleData, ...ruLocaleData, ...frLocaleData, ...esLocaleData, ...itLocaleData]);
// Our translated strings
import { ru } from './locales/ru';
import { en } from './locales/en';
import { fr } from './locales/fr';
import { es } from './locales/es';
import { it } from './locales/it';
const messages = {ru, en, fr, es, it}
/*
exported function placeholders
this is needed for proper export before react-intl functions with locale data,
will be properly created (they depend on react props and context),
which is not available until component is being created
*/
/*
this placeholder is needed for usage outside of react. In server side code and in static html files.
This function is very simple, it does NOT support dynamic values (for example: translate('your_email_is', {email: 'x@y.com'})). Use it carefully
*/
let translate = string => {
let language = DEFAULT_LANGUAGE
if (process.env.BROWSER) language = store.get('language') || DEFAULT_LANGUAGE
return messages[language][string]
};
let translateHtml = () => {}; // NOTE: translateHtml() rarely works properly, prefer using translate()
let translatePlural = () => {};
let translateNumber = () => {};
// react-intl's formatMessage and formatHTMLMessage functions depend on context(this is where strings are stored)
// thats why we:
// 1) create instance of <IntlProvider /> which wraps our application and creates react context (see "Translator" component below)
// 2) create <DummyComponentToExportProps /> inside <IntlProvider /> (the "Translator" component)
// 3) now we have proper context which we use to export translate() and translateHtml() to be used anywhere
// all of this shenanigans are needed because many times translations are needed outside of components(in reducers and redux "connect" functions)
// but since react-intl functions depends on components context it would be not possible
@injectIntl // inject translation functions through 'intl' prop
class DummyComponentToExportProps extends React.Component {
render() { // render hidden placeholder
return <span hidden>{' '}</span>
}
// ⚠️ IMPORTANT
// use 'componentWillMount' instead of 'componentDidMount',
// or there will be all sorts of partially rendered components
componentWillMount() {
// assign functions after component is created (context is picked up)
translate = (...params) => this.translateHandler('string', ...params)
translateHtml = (...params) => this.translateHandler('html', ...params)
translatePlural = (...params) => this.translateHandler('plural', ...params)
translateNumber = (...params) => this.translateHandler('number', ...params)
}
translateHandler(translateType, id, values, options) {
const { formatMessage, formatHTMLMessage, formatPlural, formatNumber } = this.props.intl
// choose which method of rendering to choose: normal string or string with html
// handler = translateType === 'string' ? formatMessage : formatHTMLMessage
let handler
switch (translateType) {
case 'string':
handler = formatMessage; break
case 'html':
handler = formatHTMLMessage; break
case 'plural':
handler = formatPlural; break
case 'number':
handler = formatNumber; break
default:
throw new Error('unknown translate handler type')
}
// check if right parameters were used before running function
if (isString(id)) {
if (!isUndefined(values) && !isObject(values)) throw new Error('translating function second parameter must be an object!');
/* map parameters for react-intl */
// 'formatNumber' uses formatNumber(value: number) structure
else if (translateType == 'number') return handler(Number(id))
// everything else uses formatMessage({id: 'stringId', values: {some: 'values'}, options: {}}) structure
else return handler({id}, values, options)
}
else throw new Error('translating function first parameter must be a string!');
}
}
// actual wrapper for application
class Translator extends React.Component {
render() {
/* LANGUAGE PICKER */
let language = this.props.locale; // usually 'en'
/*
logic: if user has not picked language (storred in LocalStorage),
pick browsers language,
and match it agains our supported languages ('messages' constant),
if language is unsupported use DEFAULT_LANGUAGE
*/
// NOTE enable this if statement if you do want to auto choose users translation options
// if (process.env.BROWSER) {
// const storredLanguage = store.get('language')
// if (storredLanguage) language = storredLanguage
// else {
// // Different browsers have the user locale defined
// // on different fields on the `navigator` object, so we make sure to account
// // for these different by checking all of them
// const browsersLanguage = navigator
// ? (navigator.languages && navigator.languages[0])
// || navigator.language
// || navigator.userLanguage
// : ''
// // Split locales with a region code (ie. 'en-EN' to 'en')
// const languageWithoutRegionCode = browsersLanguage.toLowerCase().split(/[_-]+/)[0];
// if (!messages.hasOwnProperty(languageWithoutRegionCode)) language = DEFAULT_LANGUAGE
// else language = languageWithoutRegionCode
// }
// }
return <IntlProvider
// to ensure dynamic language change, "key" property with same "locale" info must be added
// see: https://github.com/yahoo/react-intl/wiki/Components#multiple-intl-contexts
key={language}
locale={language}
messages={messages[language]}
defaultLocale={DEFAULT_LANGUAGE}
>
<div>
{/* self explanatory */}
<DummyComponentToExportProps />
{/* render actual content */}
{this.props.children}
</div>
</IntlProvider>
}
render() {
let language = this.props.locale;
return <IntlProvider
// to ensure dynamic language change, "key" property with same "locale" info must be added
// see: https://github.com/yahoo/react-intl/wiki/Components#multiple-intl-contexts
key={language}
locale={language}
defaultLocale={DEFAULT_LANGUAGE}
>
{this.props.children}
</IntlProvider>
}
}
export { translate, translateHtml, translatePlural, translateNumber }
export default connect(
// mapStateToProps
(state, ownProps) => {
const locale = state.user.get('locale')
return {...ownProps, locale}
const locale = state.user.get('locale');
return {...ownProps, locale};
}
)(Translator)
)(Translator);
export const FormattedHTMLMessage = ({id, params, className}) => (
<div className={'FormattedHTMLMessage' + (className ? ` ${className}` : '')} dangerouslySetInnerHTML={ { __html: tt(id, params) } }></div>
);
......@@ -15,6 +15,7 @@ export const VESTING_TOKEN = 'STEEM POWER';
export const INVEST_TOKEN_UPPERCASE = 'STEEM POWER';
export const INVEST_TOKEN_SHORT = 'SP';
export const DEBT_TOKEN = 'STEEM DOLLAR';
export const DEBT_TOKENS = 'STEEM DOLLARS';
export const CURRENCY_SIGN = '$';
export const WIKI_URL = ''; // https://wiki.golos.io/
export const LANDING_PAGE_URL = 'https://steem.io/';
......
......@@ -14,9 +14,10 @@ import Dialogs from 'app/components/modules/Dialogs';
import Modals from 'app/components/modules/Modals';
import Icon from 'app/components/elements/Icon';
import MiniHeader from 'app/components/modules/MiniHeader';
import { translate } from '../Translator.js';
import tt from 'counterpart';
import PageViewsCounter from 'app/components/elements/PageViewsCounter';
import {serverApiRecordEvent} from 'app/utils/ServerApiClient';
import { APP_NAME, VESTING_TOKEN, LIQUID_TOKEN } from 'app/client_config';
import {key_utils} from 'steem/lib/auth/ecc';
import resolveRoute from 'app/ResolveRoute';
......@@ -157,12 +158,12 @@ class App extends React.Component {
<ul>
<li>
<a href="https://steemit.com/steemit/@steemitblog/steemit-com-is-now-open-source">
{translate('steemit_is_now_open_source')}
{tt('submit_a_story.APP_NAME_is_now_open_source', {APP_NAME})}
</a>
</li>
<li>
<a href="https://steemit.com/steemit/@steemitblog/all-recovered-accounts-have-been-fully-refunded">
{translate("all_accounts_refunded")}
{tt('submit_a_story.all_accounts_refunded')}
</a>
</li>
</ul>
......@@ -175,7 +176,7 @@ class App extends React.Component {
<div className="column">
<div className={classNames('callout warning', {alert}, {warning}, {success})}>
<CloseButton onClick={() => this.setState({showCallout: false})} />
<p>{translate("read_only_mode")}</p>
<p>{tt('g.read_only_mode')}</p>
</div>
</div>
</div>;
......@@ -188,16 +189,16 @@ class App extends React.Component {
<div className="welcomeBanner">
<CloseButton onClick={() => this.setState({showBanner: false})} />
<div className="text-center">
<h2>{translate("welcome_to_the_blockchain")}</h2>
<h4>{translate("your_voice_is_worth_something")}</h4>
<h2>{tt('submit_a_story.welcome_to_the_blockchain')}</h2>
<h4>{tt('submit_a_story.your_voice_is_worth_something')}</h4>
<br />
<a className="button" href="/pick_account" onClick={this.signUp}> <b>{translate("sign_up")}</b> </a>
<a className="button" href="/enter_email"> <b>{tt('navigation.sign_up')}</b> </a>
&nbsp; &nbsp; &nbsp;
<a className="button hollow uppercase" href="https://steem.io" target="_blank" rel="noopener noreferrer" onClick={this.learnMore}> <b>{translate("learn_more")}</b> </a>
<a className="button hollow uppercase" href="https://steem.io" target="_blank" rel="noopener noreferrer" onClick={this.learnMore}> <b>{tt('submit_a_story.learn_more')}</b> </a>
<br />
<br />
<div className="tag3">
<b>{translate("get_sp_when_sign_up", {signupBonus: signup_bonus})}</b>
<b>{tt('submit_a_story.get_sp_when_sign_up', {signupBonus: signup_bonus, VESTING_TOKEN})}</b>
</div>
</div>
</div>
......@@ -213,81 +214,81 @@ class App extends React.Component {
<ul className="vertical menu">
<li>
<a href="/welcome" onClick={this.navigate}>
{translate("welcome")}
{tt('navigation.welcome')}
</a>
</li>
<li>
<a href="/faq.html" onClick={this.navigate}>
FAQ
{tt('navigation.faq')}
</a>
</li>
<li>
<a href="/tags" onClick={this.navigate}>
{translate("explore")}
{tt('navigation.explore')}
</a>
</li>
<li>
<a onClick={() => depositSteem(username)}>
{translate("buy_LIQUID_TOKEN")}
{tt('navigation.buy_LIQUID_TOKEN', {LIQUID_TOKEN})}
</a>
</li>
<li>
<a href="/market" onClick={this.navigate}>
{translate("currency_market")}
{tt('navigation.currency_market')}
</a>
</li>
<li>
<a href="/recover_account_step_1" onClick={this.navigate}>
{translate("stolen_account_recovery")}
{tt('navigation.stolen_account_recovery')}
</a>
</li>
<li>
<a href="/change_password" onClick={this.navigate}>
{translate("change_account_password")}
{tt('navigation.change_account_password')}
</a>
</li>
<li className="last">
<a href="/~witnesses" onClick={this.navigate}>
{translate("vote_for_witnesses")}
{tt('navigation.vote_for_witnesses')}
</a>
</li>
</ul>
<ul className="vertical menu">
<li>
<a href="https://steemit.chat/home" target="_blank" rel="noopener noreferrer">
{translate("APP_NAME_chat")}&nbsp;<Icon name="extlink" />
{tt('navigation.chat')}&nbsp;<Icon name="extlink" />
</a>
</li>
<li>
<a href="http://steemtools.com/" onClick={this.navigate} target="_blank" rel="noopener noreferrer">
{translate('APP_NAME_app_center')}&nbsp;<Icon name="extlink" />
{tt('navigation.app_center')}&nbsp;<Icon name="extlink" />
</a>
</li>
<li className="last">
<a href="https://steemit.github.io/steemit-docs/" target="_blank" rel="noopener noreferrer">
{translate("steemit_api_docs")}&nbsp;<Icon name="extlink" />
{tt('navigation.steemit_api_docs')}&nbsp;<Icon name="extlink" />
</a>
</li>
</ul>
<ul className="vertical menu">
<li>
<a href="https://steem.io/SteemWhitePaper.pdf" onClick={this.navigate}>
{translate("APP_NAME_whitepaper")}&nbsp;<Icon name="extlink" />
{tt('navigation.whitepaper')}&nbsp;<Icon name="extlink" />
</a>
</li>
<li>
<a href="https://steem.io" onClick={this.navigate}>
{translate("about")}&nbsp;<Icon name="extlink" />
{tt('navigation.about')}&nbsp;<Icon name="extlink" />
</a>
</li>
<li>
<a href="/privacy.html" onClick={this.navigate} rel="nofollow">
{translate("privacy_policy")}
{tt('navigation.privacy_policy')}
</a>
</li>
<li className="last">
<a href="/tos.html" onClick={this.navigate} rel="nofollow">
{translate("terms_of_service")}
{tt('navigation.terms_of_service')}
</a>
</li>
</ul>
......
......@@ -3,7 +3,7 @@ import {connect} from 'react-redux'
import Link from 'app/components/elements/Link'
import g from 'app/redux/GlobalReducer'
import links from 'app/utils/Links'
import { translate } from 'app/Translator';
import tt from 'counterpart';
/** @deprecated */
class CardView extends React.Component {
......@@ -44,13 +44,13 @@ class CardView extends React.Component {
const youTubeImage = links.youTube.test(link)
return <span className="Card">
{image && !youTubeImage && <div>
{canEdit && <div>(<a onClick={this.onCloseImage}>{translate('remove')}</a>)<br /></div>}
{canEdit && <div>(<a onClick={this.onCloseImage}>{tt('g.remove')}</a>)<br /></div>}
<Link href={link}>
<img src={image} alt={alt} />
</Link>
</div>}
{description && <div>
{canEdit && <span>(<a onClick={this.onCloseDescription}>{translate('remove')}</a>)</span>}
{canEdit && <span>(<a onClick={this.onCloseDescription}>{tt('g.remove')}</a>)</span>}
<Link href={link}>
<blockquote>{description}</blockquote>
</Link>
......
......@@ -2,7 +2,7 @@ import React from 'react';
import {connect} from 'react-redux'
import shouldComponentUpdate from 'app/utils/shouldComponentUpdate'
import {cleanReduxInput} from 'app/utils/ReduxForms'
import { translate } from '../../Translator.js';
import tt from 'counterpart';
class CategorySelector extends React.Component {
static propTypes = {
......@@ -60,7 +60,7 @@ class CategorySelector extends React.Component {
const categorySelect = (
<select {...cleanReduxInput(this.props)} onChange={this.categorySelectOnChange} ref="categoryRef" tabIndex={tabIndex} disabled={disabled}>
<option value="">{translate('select_a_tag')}...</option>
<option value="">{tt('category_selector_jsx.select_a_tag')}...</option>
{categoryOptions}
<option value="new">{this.props.placeholder}</option>
</select>
......@@ -73,18 +73,18 @@ class CategorySelector extends React.Component {
}
}
export function validateCategory(category, required = true) {
if(!category || category.trim() === '') return required ? translate( 'required' ) : null
if(!category || category.trim() === '') return required ? tt('g.required') : null
const cats = category.trim().split(' ')
return (
// !category || category.trim() === '' ? 'Required' :
cats.length > 5 ? translate('use_limitied_amount_of_categories', {amount: 5}) :
cats.find(c => c.length > 24) ? translate('maximum_tag_length_is_24_characters') :
cats.find(c => c.split('-').length > 2) ? translate('use_one_dash') :
cats.find(c => c.indexOf(',') >= 0) ? translate('use_spaces_to_separate_tags') :
cats.find(c => /[A-Z]/.test(c)) ? translate('use_only_lowercase_letters') :
cats.find(c => !/^[a-z0-9-#]+$/.test(c)) ? translate('use_only_allowed_characters') :
cats.find(c => !/^[a-z-#]/.test(c)) ? translate('must_start_with_a_letter') :
cats.find(c => !/[a-z0-9]$/.test(c)) ? translate('must_end_with_a_letter_or_number') :
cats.length > 5 ? tt('use_limitied_amount_of_categories', {amount: 5}) :
cats.find(c => c.length > 24) ? tt('category_selector_jsx.maximum_tag_length_is_24_characters') :
cats.find(c => c.split('-').length > 2) ? tt('category_selector_jsx.use_one_dash') :
cats.find(c => c.indexOf(',') >= 0) ? tt('category_selector_jsx.use_spaces_to_separate_tags') :
cats.find(c => /[A-Z]/.test(c)) ? tt('category_selector_jsx.use_only_lowercase_letters') :
cats.find(c => !/^[a-z0-9-#]+$/.test(c)) ? tt('category_selector_jsx.use_only_allowed_characters') :
cats.find(c => !/^[a-z-#]/.test(c)) ? tt('category_selector_jsx.must_start_with_a_letter') :
cats.find(c => !/[a-z0-9]$/.test(c)) ? tt('category_selector_jsx.must_end_with_a_letter_or_number') :
null
)
}
......@@ -92,6 +92,6 @@ export default connect((state, ownProps) => {
const trending = state.global.getIn(['tag_idx', 'trending'])
// apply translations
// they are used here because default prop can't acces intl property
const placeholder = translate('tag_your_story');
const placeholder = tt('category_selector_jsx..tag_your_story');
return { trending, placeholder, ...ownProps, }
})(CategorySelector);
......@@ -11,7 +11,7 @@ import TimeAgoWrapper from 'app/components/elements/TimeAgoWrapper';
import Userpic from 'app/components/elements/Userpic';
import transaction from 'app/redux/Transaction'
import {List} from 'immutable'
import { translate } from 'app/Translator';
import tt from 'counterpart';
import {parsePayoutAmount} from 'app/utils/ParsersAndFormatters';
import {Long} from 'bytebuffer';
......@@ -224,7 +224,7 @@ class CommentImpl extends React.Component {
const {cont} = this.props;
const dis = cont.get(this.props.content);
if (!dis) {
return <div>{translate('loading')}...</div>
return <div>{tt('g.loading')}...</div>
}
const comment = dis.toJS();
if(!comment.stats) {
......@@ -275,9 +275,9 @@ class CommentImpl extends React.Component {
controls = <div>
<Voting post={post} />
<span className="Comment__footer__controls">
{showReplyOption && <a onClick={onShowReply}>{translate('reply')}</a>}
{' '}{!readonly && showEditOption && <a onClick={onShowEdit}>{translate('edit')}</a>}
{' '}{!readonly && showDeleteOption && <a onClick={onDeletePost}>{translate('delete')}</a>}
{showReplyOption && <a onClick={onShowReply}>{tt('g.reply')}</a>}
{' '}{!readonly && showEditOption && <a onClick={onShowEdit}>{tt('g.edit')}</a>}
{' '}{!readonly && showDeleteOption && <a onClick={onDeletePost}>{tt('g.delete')}</a>}
</span>
</div>;
}
......@@ -350,7 +350,7 @@ class CommentImpl extends React.Component {
<div className="Comment__header">
<div className="Comment__header_collapse">
<Voting post={post} flag />
<a title={translate('collapse_or_expand')} onClick={this.toggleCollapsed}>{ this.state.collapsed ? '[+]' : '[-]' }</a>
<a title={tt('blocktrades_deposit.collapse_or_expand')} onClick={this.toggleCollapsed}>{ this.state.collapsed ? '[+]' : '[-]' }</a>
</div>
<span className="Comment__header-user">
<div className="Comment__Userpic-small">
......@@ -365,9 +365,9 @@ class CommentImpl extends React.Component {
{ (this.state.collapsed || hide_body) &&
<Voting post={post} showList={false} /> }
{ this.state.collapsed && comment.children > 0 &&
<span className="marginLeft1rem">{translate('reply_count', {replyCount: comment.children})}</span>}
<span className="marginLeft1rem">{tt('reply_count', {replyCount: comment.children})}</span>}
{ !this.state.collapsed && hide_body &&
<a className="marginLeft1rem" onClick={this.revealBody}>{translate('reveal_comment')}</a>}
<a className="marginLeft1rem" onClick={this.revealBody}>{tt('blocktrades_deposit.reveal_comment')}</a>}
</div>
<div className="Comment__body entry-content">
{showEdit ? renderedEditor : body}
......@@ -408,7 +408,7 @@ const Comment = connect(
dispatch(transaction.actions.broadcastOperation({
type: 'delete_comment',
operation: {author, permlink},
confirm: translate('are_you_sure'),
confirm: tt('blocktrades_deposit.are_you_sure'),
}))
},
})
......
......@@ -4,9 +4,9 @@ import {Component} from 'react'
import Remarkable from 'remarkable'
import YoutubePreview from 'app/components/elements/YoutubePreview'
import sanitizeConfig, {noImageText} from 'app/utils/SanitizeConfig'
import {renderToString} from 'react-dom/server';
import sanitize from 'sanitize-html'
import HtmlReady from 'shared/HtmlReady'
import tt from 'counterpart';
const remarkable = new Remarkable({
html: true, // remarkable renders first then sanitize runs...
......@@ -136,8 +136,8 @@ class MarkdownViewer extends Component {
{sections}
{noImageActive && allowNoImage &&
<div onClick={this.onAllowNoImage} className="MarkdownViewer__negative_group">
Images were hidden due to low ratings.
<button style={{marginBottom: 0}} className="button hollow tiny float-right">Show</button>
{tt('markdownviewer_jsx.images_were_hidden_due_to_low_ratings')}
<button style={{marginBottom: 0}} className="button hollow tiny float-right">{tt('g.show')}</button>
</div>
}
</div>)
......
......@@ -20,7 +20,8 @@ import PageViewsCounter from 'app/components/elements/PageViewsCounter';
import ShareMenu from 'app/components/elements/ShareMenu';
import {serverApiRecordEvent} from 'app/utils/ServerApiClient';
import Userpic from 'app/components/elements/Userpic';
import { APP_DOMAIN } from 'app/client_config';
import { APP_DOMAIN, APP_NAME } from 'app/client_config';
import tt from 'counterpart';
import userIllegalContent from 'app/utils/userIllegalContent';
// function loadFbSdk(d, s, id) {
......@@ -49,10 +50,10 @@ function TimeAuthorCategory({content, authorRepLog10, showTags}) {
<span className="PostFull__time_author_category vcard">
<Icon name="clock" className="space-right" />
<TimeAgoWrapper date={content.created} className="updated" />
{} by <Author author={content.author} authorRepLog10={authorRepLog10} />
{showTags && <span> in <TagList post={content} single /></span>}
{} {tt('g.by')} <Author author={content.author} authorRepLog10={authorRepLog10} />
{showTags && <span> {tt('g.in')} <TagList post={content} single /></span>}
</span>
);
);
}
function TimeAuthorCategoryLarge({content, authorRepLog10}) {
......@@ -62,7 +63,7 @@ function TimeAuthorCategoryLarge({content, authorRepLog10}) {
<Userpic account={content.author} />
<div className="right-side">
<Author author={content.author} authorRepLog10={authorRepLog10} />
<span> in <TagList post={content} single /></span>
<span> {tt('g.in')} <TagList post={content} single /></span>
</div>
</span>
);
......@@ -139,13 +140,13 @@ class PostFull extends React.Component {
const href = this.share_params.url;
e.preventDefault();
// loadFbSdk(document, 'script', 'facebook-jssdk').then(fb => {
window.FB.ui({
method: 'share',
href
}, (response) => {
if (response && !response.error_message)
serverApiRecordEvent('FbShare', this.share_params.link);
});
window.FB.ui({
method: 'share',
href
}, (response) => {
if (response && !response.error_message)
serverApiRecordEvent('FbShare', this.share_params.link);
});
// });
}
......@@ -200,12 +201,12 @@ class PostFull extends React.Component {
if (content.category) link = `/${content.category}${link}`;
const {category, title, body} = content