Skip to content
Snippets Groups Projects
Commit c577983b authored by Valentine Zavgorodnev's avatar Valentine Zavgorodnev Committed by Jeffrey Paul
Browse files

add i18n support (#430)

* merge i18n related changes from steemit-intl #233

* Readme typo
parent 60fcea86
No related branches found
No related tags found
No related merge requests found
import React from 'react';
import isString from 'lodash/isString';
import isObject from 'lodash/isObject';
import isUndefined from 'lodash/isUndefined';
import { IntlProvider, addLocaleData, injectIntl } from 'react-intl';
// most of this code creates a wrapper for i18n API.
// this is needed to make i18n future proof
/*
module exports two functions: translate and translateHtml
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 { en } from './locales/en';
import { ru } from './locales/ru';
import { fr } from './locales/fr';
import { es } from './locales/es';
import { it } from './locales/it';
const translations = {
en: en,
ru: ru,
fr: fr,
es: es,
it: 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)
let translate = () => {};
let translateHtml = () => {};
let translatePlural = () => {};
// 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
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 renddered 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)
}
translateHandler(translateType, id, values, options) {
const { formatMessage, formatHTMLMessage, formatPlural } = 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
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,
// which 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!');
}
}
// inject translation functions through 'intl' prop (not using decorator)
DummyComponentToExportProps = injectIntl(DummyComponentToExportProps)
// actual wrapper for application
class Translator extends React.Component {
render() {
/* LANGUAGE PICKER */
// Define user's language. 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
let language = 'en';
// while Server Side Rendering is in process, 'navigator' is undefined
if (process.env.BROWSER) {
language = navigator ? (navigator.languages && navigator.languages[0])
|| navigator.language
|| navigator.userLanguage
: 'en';
}
//Split locales with a region code (ie. 'en-EN' to 'en')
const languageWithoutRegionCode = language.toLowerCase().split(/[_-]+/)[0];
// TODO: don't forget to add Safari polyfill
// 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
let messages = translations[languageWithoutRegionCode]
return <IntlProvider locale={languageWithoutRegionCode} key={languageWithoutRegionCode} messages={messages}>
<div>
<DummyComponentToExportProps />
{this.props.children}
</div>
</IntlProvider>
}
}
export { translate, translateHtml, translatePlural }
export default Translator
...@@ -15,6 +15,7 @@ import Modals from 'app/components/modules/Modals'; ...@@ -15,6 +15,7 @@ import Modals from 'app/components/modules/Modals';
import Icon from 'app/components/elements/Icon'; import Icon from 'app/components/elements/Icon';
import {key_utils} from 'shared/ecc'; import {key_utils} from 'shared/ecc';
import MiniHeader from 'app/components/modules/MiniHeader'; import MiniHeader from 'app/components/modules/MiniHeader';
import { translate } from '../Translator.js';
class App extends React.Component { class App extends React.Component {
constructor(props) { constructor(props) {
...@@ -101,12 +102,12 @@ class App extends React.Component { ...@@ -101,12 +102,12 @@ class App extends React.Component {
<ul> <ul>
<li> <li>
<a href="https://steemit.com/steemit/@steemitblog/steemit-com-is-now-open-source"> <a href="https://steemit.com/steemit/@steemitblog/steemit-com-is-now-open-source">
Steemit.com is now Open Source {translate('steemit_is_now_open_source')}
</a> </a>
</li> </li>
<li> <li>
<a href="https://steemit.com/steemit/@steemitblog/all-recovered-accounts-have-been-fully-refunded"> <a href="https://steemit.com/steemit/@steemitblog/all-recovered-accounts-have-been-fully-refunded">
All Recovered Accounts have been fully Refunded {translate("all_accounts_refunded")}
</a> </a>
</li> </li>
</ul> </ul>
...@@ -119,7 +120,7 @@ class App extends React.Component { ...@@ -119,7 +120,7 @@ class App extends React.Component {
<div className="column"> <div className="column">
<div className={classNames('callout warning', {alert}, {warning}, {success})}> <div className={classNames('callout warning', {alert}, {warning}, {success})}>
<CloseButton onClick={() => this.setState({showCallout: false})} /> <CloseButton onClick={() => this.setState({showCallout: false})} />
<p>Due to server maintenance we are running in read only mode. We are sorry for the inconvenience.</p> <p>{translate("read_only_mode")}</p>
</div> </div>
</div> </div>
</div>; </div>;
...@@ -132,16 +133,16 @@ class App extends React.Component { ...@@ -132,16 +133,16 @@ class App extends React.Component {
<div className="welcomeBanner"> <div className="welcomeBanner">
<CloseButton onClick={() => this.setState({showBanner: false})} /> <CloseButton onClick={() => this.setState({showBanner: false})} />
<div className="text-center"> <div className="text-center">
<h2>Welcome to the Blockchain!</h2> <h2>{translate("welcome_to_the_blockchain")}</h2>
<h4>Your voice is worth something</h4> <h4>{translate("your_voice_is_worth_something")}</h4>
<br /> <br />
<a className="button" href="/create_account" onClick={showSignUp}> <b>SIGN UP</b> </a> <a className="button" href="/create_account" onClick={showSignUp}> <b>{translate("sign_up")}</b> </a>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
<a className="button hollow" href="https://steem.io" target="_blank"> <b>LEARN MORE</b> </a> <a className="button hollow uppercase" href="https://steem.io" target="_blank"> <b>{translate("learn_more")}</b> </a>
<br /> <br />
<br /> <br />
<div className="tag3"> <div className="tag3">
<b>Get {signup_bonus} of Steem Power when you sign up today.</b> <b>{translate("get_sp_when_sign_up", {signupBonus: signup_bonus})}</b>
</div> </div>
</div> </div>
</div> </div>
...@@ -154,22 +155,68 @@ class App extends React.Component { ...@@ -154,22 +155,68 @@ class App extends React.Component {
<SidePanel ref="side_panel" alignment="right"> <SidePanel ref="side_panel" alignment="right">
<TopRightMenu vertical navigate={this.navigate} /> <TopRightMenu vertical navigate={this.navigate} />
<ul className="vertical menu"> <ul className="vertical menu">
<li><a href="https://steem.io" onClick={this.navigate}>About</a></li> <li>
<li><a href="/tags.html/hot" onClick={this.navigate}>Explore</a></li> <a href="https://steem.io" onClick={this.navigate}>
<li><a href="https://steem.io/SteemWhitePaper.pdf" onClick={this.navigate}>Steem Whitepaper</a></li> {translate("about")}
<li><a onClick={() => depositSteem()}>Buy Steem</a></li> </a>
<li><a href="/market" onClick={this.navigate}>Market</a></li> </li>
<li><a href="http://steemtools.com/" onClick={this.navigate}>Steem App Center</a></li> <li>
<li><a href="/recover_account_step_1" onClick={this.navigate}>Stolen Account Recovery</a></li> <a href="/tags.html/hot" onClick={this.navigate}>
<li><a href="/change_password" onClick={this.navigate}>Change Account Password</a></li> {translate("explore")}
<li><a href="https://steemit.chat/home" target="_blank">Steemit Chat&nbsp;<Icon name="extlink"/></a></li> </a>
<li className="last"><a onClick={this.navigate} href="/~witnesses">Witnesses</a></li> </li>
<li className="last"><a onClick={this.navigate} href="/@steemitjobs">Careers</a></li> <li>
<li className="last"><a href="mailto:contact@steemit.com">Contact Steemit</a></li> <a href="https://steem.io/SteemWhitePaper.pdf" onClick={this.navigate}>
{translate("whitepaper")}
</a>
</li>
<li>
<a onClick={() => depositSteem()}>
{translate("buy_steem")}
</a>
</li>
<li>
<a href="http://steemtools.com/" onClick={this.navigate}>
{translate('steem_app_center')}
</a>
</li>
<li>
<a href="/market" onClick={this.navigate}>
{translate("market")}
</a>
</li>
<li>
<a href="/recover_account_step_1" onClick={this.navigate}>
{translate("stolen_account_recovery")}
</a>
</li>
<li>
<a href="/change_password" onClick={this.navigate}>
{translate("change_account_password")}
</a>
</li>
<li>
<a href="https://steemit.chat/home" target="_blank">
{translate("steemit_chat")}&nbsp;<Icon name="extlink" />
</a>
</li>
<li className="last">
<a href="/~witnesses" onClick={this.navigate}>
{translate("witnesses")}
</a>
</li>
</ul> </ul>
<ul className="vertical menu"> <ul className="vertical menu">
<li><a href="/privacy.html" onClick={this.navigate} rel="nofollow">Privacy Policy</a></li> <li>
<li><a href="/tos.html" onClick={this.navigate} rel="nofollow">Terms of Service</a></li> <a href="/privacy.html" onClick={this.navigate} rel="nofollow">
{translate("privacy_policy")}
</a>
</li>
<li>
<a href="/tos.html" onClick={this.navigate} rel="nofollow">
{translate("terms_of_service")}
</a>
</li>
</ul> </ul>
</SidePanel> </SidePanel>
{miniHeader ? <MiniHeader /> : <Header toggleOffCanvasMenu={this.toggleOffCanvasMenu} menuOpen={this.state.open} />} {miniHeader ? <MiniHeader /> : <Header toggleOffCanvasMenu={this.toggleOffCanvasMenu} menuOpen={this.state.open} />}
...@@ -181,7 +228,7 @@ class App extends React.Component { ...@@ -181,7 +228,7 @@ class App extends React.Component {
</div> </div>
<Dialogs /> <Dialogs />
<Modals /> <Modals />
</div>; </div>
} }
} }
......
# internationalization guide
## how to add your own language
1. copy ./en.js
2. rename it (for example jp.js)
3. translate it
5. go to server-html.jsx
6. add your locale date as it is done in https://cdn.polyfill.io script (add ',Intl.~locale.XX' at the end of url)
7. add localeData and newly translated strings as it is done in Translator.jsx (read the comments)
## Notes for hackers and translators
'keep_syntax_lowercase_with_dashes' on string names. Example: show_password: 'Show Password'
Please keep in mind that none of the strings are bind directly to components to keep them reusable. For example: 'messages_count' can be used in one page today, but also can be placed in another tomorrow.
Strings must be as close to source as possible.
They must keep proper structure because "change_password" can translate both 'Change Password' and 'Change Account Password'.
Same with "user hasn't started posting" and "user hasn't started posting yet", 'user_hasnt_followed_anything_yet' and 'user_hasnt_followed_anything' is not the same
### About syntax rules
Do not use anything to style strings unless you are 100% sure what you are doing.
This is no good: 'LOADING' instead of 'Loading'. Avoid whitespace styling: ' Loading' - is no good.
Also, try to avoid using syntax signs, unless it's a long ass string which will always end with dot no matter where you put it. Example: 'Loading...', 'Loading!' is no good, use 'Loading' instead.
If you are not sure which syntax to use and how to write your translations just copy the way original string have been written.
### How to use plurals
Plurals are strings which look differently depending on what numbers are used.
We use formatJs syntax, read the docs http://formatjs.io/guides/message-syntax/
Pay special attention to '{plural} Format' section.
[This link](http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html) shows how they determine which plural to use in different languages (they explain how string falls under 'few', 'many' and 'other' category. If you are completely lost, just look at the other translation files (en.js or ru.js).
### How to use special symbols
\n means new line break
\' means ' (single quote sign)
this works: 'hasn\'t', "hasn't" (double quotes > single quotes)
this does not: 'hasn't'
Some languages require certain strings to be empty. For example, Russian language in some context does not have equivalent for 'by'('Post created by Misha'). For empty strings use ' '(empty quotes with single space) instead of '', otherwise you will see string name instead of nothing.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -26,6 +26,7 @@ ...@@ -26,6 +26,7 @@
"babel-core": "^6.8.0", "babel-core": "^6.8.0",
"babel-eslint": "^6.0.4", "babel-eslint": "^6.0.4",
"babel-loader": "^6.2.1", "babel-loader": "^6.2.1",
"babel-plugin-react-intl": "^2.2.0",
"babel-plugin-transform-runtime": "^6.8.0", "babel-plugin-transform-runtime": "^6.8.0",
"babel-preset-es2015": "^6.3.13", "babel-preset-es2015": "^6.3.13",
"babel-preset-react": "^6.3.13", "babel-preset-react": "^6.3.13",
...@@ -86,6 +87,7 @@ ...@@ -86,6 +87,7 @@
"react-dom": "^15.3.2", "react-dom": "^15.3.2",
"react-foundation-components": "git+https://github.com/valzav/react-foundation-components.git#lib", "react-foundation-components": "git+https://github.com/valzav/react-foundation-components.git#lib",
"react-highcharts": "^8.3.3", "react-highcharts": "^8.3.3",
"react-intl": "^2.1.3",
"react-medium-editor": "^1.8.0", "react-medium-editor": "^1.8.0",
"react-notification": "^5.0.7", "react-notification": "^5.0.7",
"react-overlays": "0.6.4", "react-overlays": "0.6.4",
......
...@@ -28,6 +28,7 @@ import PollDataSaga from 'app/redux/PollDataSaga'; ...@@ -28,6 +28,7 @@ import PollDataSaga from 'app/redux/PollDataSaga';
import {component as NotFound} from 'app/components/pages/NotFound'; import {component as NotFound} from 'app/components/pages/NotFound';
import extractMeta from 'app/utils/ExtractMeta'; import extractMeta from 'app/utils/ExtractMeta';
import {serverApiRecordEvent} from 'app/utils/ServerApiClient'; import {serverApiRecordEvent} from 'app/utils/ServerApiClient';
import Translator from 'app/Translator';
const sagaMiddleware = createSagaMiddleware( const sagaMiddleware = createSagaMiddleware(
...userWatches, // keep first to remove keys early when a page change happens ...userWatches, // keep first to remove keys early when a page change happens
...@@ -120,11 +121,13 @@ async function universalRender({ location, initial_state, offchain }) { ...@@ -120,11 +121,13 @@ async function universalRender({ location, initial_state, offchain }) {
} }
return render( return render(
<Provider store={store}> <Provider store={store}>
<Translator>
<Router <Router
routes={RootRoute} routes={RootRoute}
history={history} history={history}
onError={onRouterError} onError={onRouterError}
render={applyRouterMiddleware(scroll)} /> render={applyRouterMiddleware(scroll)} />
</Translator>
</Provider>, </Provider>,
document.getElementById('content') document.getElementById('content')
); );
...@@ -169,7 +172,9 @@ async function universalRender({ location, initial_state, offchain }) { ...@@ -169,7 +172,9 @@ async function universalRender({ location, initial_state, offchain }) {
try { try {
app = renderToString( app = renderToString(
<Provider store={server_store}> <Provider store={server_store}>
<Translator>
<RouterContext { ...renderProps } /> <RouterContext { ...renderProps } />
</Translator>
</Provider> </Provider>
); );
meta = extractMeta(onchain, renderProps.params); meta = extractMeta(onchain, renderProps.params);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment