diff --git a/package.json b/package.json
index 122e87a..23ffa7b 100644
--- a/package.json
+++ b/package.json
@@ -8,10 +8,11 @@
"license": "UNLICENSED",
"repository": "git@bitbucket.org:ErickSkrauch/ely.by-account.git",
"scripts": {
- "start": "webpack-dev-server --progress --colors",
+ "start": "rm -rf dist/ && webpack-dev-server --progress --colors",
"up": "npm install",
"test": "karma start ./karma.conf.js",
"lint": "eslint ./src",
+ "i18n": "cd ./scripts && ./node_modules/.bin/babel-node i18n-collect.js",
"build": "rm -rf dist/ && webpack --progress --colors -p"
},
"dependencies": {
@@ -44,6 +45,7 @@
"babel-preset-react-hmre": "^1.0.1",
"babel-preset-stage-0": "^6.3.13",
"babel-runtime": "^5.6.15",
+ "bundle-loader": "^0.5.4",
"chai": "^3.0.0",
"chokidar": "^1.2.0",
"css-loader": "^0.23.0",
diff --git a/scripts/.babelrc b/scripts/.babelrc
new file mode 100644
index 0000000..0522a89
--- /dev/null
+++ b/scripts/.babelrc
@@ -0,0 +1,3 @@
+{
+ "breakConfig": true
+}
diff --git a/scripts/i18n-collect.js b/scripts/i18n-collect.js
new file mode 100644
index 0000000..b0a22b3
--- /dev/null
+++ b/scripts/i18n-collect.js
@@ -0,0 +1,147 @@
+import fs from 'fs';
+import {sync as globSync} from 'glob';
+import {sync as mkdirpSync} from 'mkdirp';
+import chalk from 'chalk';
+import prompt from 'prompt';
+
+const MESSAGES_PATTERN = `../dist/messages/**/*.json`;
+const LANG_DIR = `../src/i18n`;
+const DEFAULT_LOCALE = 'en';
+const SUPPORTED_LANGS = [DEFAULT_LOCALE].concat('ru');
+
+/**
+ * Aggregates the default messages that were extracted from the app's
+ * React components via the React Intl Babel plugin. An error will be thrown if
+ * there are messages in different components that use the same `id`. The result
+ * is a flat collection of `id: message` pairs for the app's default locale.
+ */
+let idToFileMap = {};
+let duplicateIds = [];
+let defaultMessages = globSync(MESSAGES_PATTERN)
+ .map((filename) => [filename, JSON.parse(fs.readFileSync(filename, 'utf8'))])
+ .reduce((collection, [file, descriptors]) => {
+ descriptors.forEach(({id, defaultMessage}) => {
+ if (collection.hasOwnProperty(id)) {
+ duplicateIds.push(id);
+ }
+
+ collection[id] = defaultMessage;
+ idToFileMap[id] = (idToFileMap[id] || []).concat(file);
+ });
+
+ return collection;
+ }, {});
+
+if (duplicateIds.length) {
+ console.log('\nFound duplicated ids:');
+ duplicateIds.forEach((id) => console.log(`${chalk.yellow(id)}:\n - ${idToFileMap[id].join('\n - ')}\n`));
+ console.log(chalk.red('Please correct the errors above to proceed further!'));
+ return;
+}
+
+duplicateIds = null;
+idToFileMap = null;
+
+/**
+ * Making a diff with the previous DEFAULT_LOCALE version
+ */
+const defaultMessagesPath = `${LANG_DIR}/${DEFAULT_LOCALE}.json`;
+let keysToUpdate = [];
+let keysToAdd = [];
+let keysToRemove = [];
+const isNotMarked = (value) => value.slice(0, 2) !== '--';
+try {
+ const prevMessages = JSON.parse(fs.readFileSync(defaultMessagesPath, 'utf8'));
+ keysToAdd = Object.keys(defaultMessages).filter((key) => !prevMessages[key]);
+ keysToRemove = Object.keys(prevMessages).filter((key) => !defaultMessages[key]).filter(isNotMarked);
+ keysToUpdate = Object.entries(prevMessages).reduce((acc, [key, message]) =>
+ acc.concat(defaultMessages[key] && defaultMessages[key] !== message ? key : [])
+ , []);
+} catch(e) {
+ console.log(chalk.yellow(`Can not read ${defaultMessagesPath}. The new file will be created.`), e);
+}
+
+if (!keysToAdd.length && !keysToRemove.length && !keysToUpdate.length) {
+ return console.log(chalk.green('Everything is up to date!'));
+}
+
+console.log(chalk.magenta(`The diff relative to default locale (${DEFAULT_LOCALE}) is:`));
+
+if (keysToRemove.length) {
+ console.log('The following keys will be removed:');
+ console.log(chalk.red('\n - ') + keysToRemove.join(chalk.red('\n - ')) + '\n');
+}
+
+if (keysToAdd.length) {
+ console.log('The following keys will be added:');
+ console.log(chalk.green('\n + ') + keysToAdd.join(chalk.green('\n + ')) + '\n');
+}
+
+if (keysToUpdate.length) {
+ console.log('The following keys will be updated:');
+ console.log(chalk.yellow('\n @ ') + keysToUpdate.join(chalk.yellow('\n @ ')) + '\n');
+}
+
+prompt.start();
+prompt.get({
+ properties: {
+ apply: {
+ description: 'Apply changes? [Y/n]',
+ pattern: /^y|n$/i,
+ message: 'Please enter "y" or "n"',
+ default: 'y',
+ before: (value) => value.toLowerCase() === 'y'
+ }
+ }
+}, (err, resp) => {
+ console.log('\n');
+
+ if (err || !resp.apply) {
+ return console.log(chalk.red('Aborted'));
+ }
+
+ buildLocales();
+
+ console.log(chalk.green('All locales was successfuly built'));
+});
+
+
+function buildLocales() {
+ mkdirpSync(LANG_DIR);
+
+ SUPPORTED_LANGS.map((lang) => {
+ const destPath = `${LANG_DIR}/${lang}.json`;
+
+ let newMessages = {};
+ try {
+ newMessages = JSON.parse(fs.readFileSync(defaultMessagesPath, 'utf8'));
+ } catch (e) {
+ console.log(chalk.yellow(`Can not read ${defaultMessagesPath}. The new file will be created.`), e);
+ }
+
+ keysToRemove.forEach((key) => {
+ delete newMessages[key];
+ });
+ keysToUpdate.forEach((key) => {
+ newMessages[`--${key}`] = newMessages[key];
+ });
+ keysToAdd.concat(keysToUpdate).forEach((key) => {
+ newMessages[key] = defaultMessages[key];
+ });
+
+ const sortedKeys = Object.keys(newMessages).sort((a, b) => {
+ a = a.replace(/^\-+/, '');
+ b = b.replace(/^\-+/, '');
+
+ return a < b || !isNotMarked(a) ? -1 : 1;
+ });
+
+ const sortedNewMessages = sortedKeys.reduce((acc, key) => {
+ acc[key] = newMessages[key];
+
+ return acc;
+ }, {});
+
+ fs.writeFileSync(destPath, JSON.stringify(sortedNewMessages, null, 4) + '\n');
+ });
+}
diff --git a/scripts/package.json b/scripts/package.json
new file mode 100644
index 0000000..b5b80ea
--- /dev/null
+++ b/scripts/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "scripts",
+ "version": "1.0.0",
+ "description": "",
+ "main": "i18n-build.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "chalk": "^1.1.3"
+ }
+}
diff --git a/src/components/auth/login/Login.jsx b/src/components/auth/login/Login.jsx
index 61d482a..375607d 100644
--- a/src/components/auth/login/Login.jsx
+++ b/src/components/auth/login/Login.jsx
@@ -8,7 +8,7 @@ import buttons from 'components/ui/buttons.scss';
import { Input } from 'components/ui/form';
import BaseAuthBody from 'components/auth/BaseAuthBody';
-import passwordMessages from 'components/auth/password/Password.messages';
+import passwordMessages from 'components/auth/password/Password.intl.json';
import messages from './Login.messages';
class Body extends BaseAuthBody {
diff --git a/src/components/auth/password/Password.intl.json b/src/components/auth/password/Password.intl.json
new file mode 100644
index 0000000..2503324
--- /dev/null
+++ b/src/components/auth/password/Password.intl.json
@@ -0,0 +1,10 @@
+{
+ "passwordTitle": "Enter password",
+ "signInButton": "Sign in",
+ "invalidPassword": "You entered wrong account password.",
+ "suggestResetPassword": "Are you have {link}?",
+ "forgotYourPassword": "forgot your password",
+ "forgotPassword": "Forgot password",
+ "accountPassword": "Account password",
+ "rememberMe": "Remember me on this device"
+}
diff --git a/src/components/auth/password/Password.jsx b/src/components/auth/password/Password.jsx
index 2458ab8..d2c1587 100644
--- a/src/components/auth/password/Password.jsx
+++ b/src/components/auth/password/Password.jsx
@@ -10,7 +10,7 @@ import { Input, Checkbox } from 'components/ui/form';
import BaseAuthBody from 'components/auth/BaseAuthBody';
import styles from './password.scss';
-import messages from './Password.messages';
+import messages from './Password.intl.json';
class Body extends BaseAuthBody {
static displayName = 'PasswordBody';
diff --git a/src/components/auth/password/Password.messages.js b/src/components/auth/password/Password.messages.js
deleted file mode 100644
index 9dfc523..0000000
--- a/src/components/auth/password/Password.messages.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import { defineMessages } from 'react-intl';
-
-export default defineMessages({
- passwordTitle: {
- id: 'passwordTitle',
- defaultMessage: 'Enter password'
- },
-
- signInButton: {
- id: 'signInButton',
- defaultMessage: 'Sign in'
- },
-
- invalidPassword: {
- id: 'invalidPassword',
- defaultMessage: 'You entered wrong account password.'
- },
-
- suggestResetPassword: {
- id: 'suggestResetPassword',
- defaultMessage: 'Are you have {link}?'
- },
-
- forgotYourPassword: {
- id: 'forgotYourPassword',
- defaultMessage: 'forgot your password'
- },
-
- forgotPassword: {
- id: 'forgotPassword',
- defaultMessage: 'Forgot password'
- },
-
- accountPassword: {
- id: 'accountPassword',
- defaultMessage: 'Account password'
- },
-
- rememberMe: {
- id: 'rememberMe',
- defaultMessage: 'Remember me on this device'
- }
-});
diff --git a/src/components/auth/register/Register.jsx b/src/components/auth/register/Register.jsx
index 68056eb..5f85b74 100644
--- a/src/components/auth/register/Register.jsx
+++ b/src/components/auth/register/Register.jsx
@@ -7,6 +7,7 @@ import buttons from 'components/ui/buttons.scss';
import { Input, Checkbox } from 'components/ui/form';
import BaseAuthBody from 'components/auth/BaseAuthBody';
+import passwordMessages from 'components/auth/password/Password.intl.json';
import activationMessages from 'components/auth/activation/Activation.messages';
import messages from './Register.messages';
@@ -44,7 +45,7 @@ class Body extends BaseAuthBody {
color="blue"
type="password"
required
- placeholder={messages.accountPassword}
+ placeholder={passwordMessages.accountPassword}
/>
Please, change password.",
+ "passCodeToApp": "To complete authorization process, please, provide the following code to {appName}",
+ "password": "Password",
+ "passwordChangeMessage": "To enhance the security of your account, please change your password.",
+ "passwordRequired": "Please enter password",
+ "passwordTooShort": "Your password is too short",
+ "passwordsDoesNotMatch": "The passwords does not match",
+ "permissionsTitle": "Application permissions",
+ "personalData": "Personal data",
+ "pleaseEnterPassword": "Please, enter your current password",
+ "preferencesDescription": "Here you can change the key preferences of your account. Please note that all actions must be confirmed by entering a password.",
+ "pressButtonToStart": "Press the button below to send a message with the code for E-mail change initialization.",
+ "rePasswordRequired": "Please retype your password",
+ "register": "Join",
+ "registerTitle": "Sign Up",
+ "repeatPassword": "Repeat password",
+ "rulesAgreementRequired": "You must accept rules in order to create an account",
+ "scope_minecraft_server_session": "Authorization data for minecraft server",
+ "scope_offline_access": "Access to your profile data, when you offline",
+ "sendEmailButton": "Send E-mail",
+ "sendMail": "Send mail",
+ "signUpButton": "Register",
+ "skipThisStep": "Skip password changing",
+ "suggestResetPassword": "Are you have {link}?",
+ "termsOfService": "Terms of service",
+ "theAppNeedsAccess1": "This application needs access",
+ "theAppNeedsAccess2": "to your data",
+ "title": "Confirm your action",
+ "twoFactorAuth": "Two factor auth",
+ "usernameRequired": "Username is required",
+ "usernameUnavailable": "This username is already taken",
+ "waitAppReaction": "Please, wait till your application response",
+ "youAuthorizedAs": "You authorized as:",
+ "yourEmail": "Your E-mail",
+ "yourNickname": "Your nickname"
+}
diff --git a/src/i18n/ru.json b/src/i18n/ru.json
new file mode 100644
index 0000000..2a94c66
--- /dev/null
+++ b/src/i18n/ru.json
@@ -0,0 +1,111 @@
+{
+ "acceptRules": "I agree with {link}",
+ "accountActivationTitle": "Account activation",
+ "accountDescription": "Ely.by account allows you to get access to many Minecraft resources. Please, take care of your account safety. Use secure password and change it regularly.",
+ "accountEmail": "Enter account E-mail",
+ "accountPassword": "Account password",
+ "accountPreferencesTitle": "Ely.by account preferences",
+ "activationMailWasSent": "Please check {email} for the message with the last registration step",
+ "alreadyReceivedCode": "Already received code",
+ "approve": "Approve",
+ "authForAppFailed": "Authorization for {appName} was failed",
+ "authForAppSuccessful": "Authorization for {appName} was successfully completed",
+ "change": "Change",
+ "changeEmailButton": "Change E-mail",
+ "changeEmailDescription": "To change current account E-mail you must first verify that you own the current address and then confirm the new one.",
+ "changeEmailTitle": "Change E-mail",
+ "changePasswordTitle": "Change password",
+ "changeUsernameButton": "Change nickname",
+ "changeUsernameDescription": "You can change your nickname to any arbitrary value. Remember that it is not recommended to take a nickname of already existing Mojang account.",
+ "changeUsernameTitle": "Change nickname",
+ "changeUsernameWarning": "Be careful: if you playing on the server with nickname binding, then after changing nickname you may lose all your progress.",
+ "changedAt": "Changed {at}",
+ "codePlaceholder": "Paste the code here",
+ "components.auth.password.accountPassword": "Account password",
+ "components.auth.password.forgotPassword": "Forgot password",
+ "components.auth.password.forgotYourPassword": "forgot your password",
+ "components.auth.password.invalidPassword": "You entered wrong account password.",
+ "components.auth.password.passwordTitle": "Enter password",
+ "components.auth.password.rememberMe": "Remember me on this device",
+ "components.auth.password.signInButton": "Sign in",
+ "components.auth.password.suggestResetPassword": "Are you have {link}?",
+ "components.profile.changePassword.achievementLossWarning": "Вы ведь дорожите своими игровыми достижениями?",
+ "components.profile.changePassword.changePasswordButton": "Сменить пароль",
+ "components.profile.changePassword.changePasswordDescription": "Придумайте пароль, который будет отличаться от ваших паролей на других сайтах и не будет совпадаеть с тем паролем, который вы используете для входа на различные игровые сервера Minecraft.",
+ "components.profile.changePassword.changePasswordTitle": "Смена пароля",
+ "components.profile.changePassword.logoutOnAllDevices": "Вылогиниться на всех устройствах",
+ "components.profile.changePassword.newPasswordLabel": "Новый пароль:",
+ "components.profile.changePassword.passwordRequirements": "Пароль должен содержать не менее 8 символов. Это могут быть любым символы — не ограничивайте себя, придумайте непредсказуемый пароль!",
+ "components.profile.changePassword.repeatNewPasswordLabel": "Повторите указанный пароль:",
+ "confirmEmail": "Confirm E-mail",
+ "contactSupport": "Contact support",
+ "copy": "Copy",
+ "currentAccountEmail": "Current account E-mail address:",
+ "currentPassword": "Enter current password",
+ "decline": "Decline",
+ "didNotReceivedEmail": "Did not received E-mail?",
+ "disabled": "Disabled",
+ "emailInvalid": "Email is invalid",
+ "emailIsTempmail": "Tempmail E-mail addresses is not allowed",
+ "emailNotAvailable": "This email is already registered.",
+ "emailOrUsername": "E-mail or username",
+ "emailRequired": "Email is required",
+ "enterFinalizationCode": "The E-mail change confirmation code was sent to {email}. Please enter the code received into the field below:",
+ "enterInitializationCode": "The E-mail with an initialization code for E-mail change procedure was sent to {email}. Please enter the code into the field below:",
+ "enterNewEmail": "Then provide your new E-mail address, that you want to use with this account. You will be mailed with confirmation code.",
+ "enterTheCode": "Enter the code from E-mail here",
+ "forgotPasswordMessage": "Specify the registration E-mail address for your account and we will send an email with instructions for further password recovery.",
+ "forgotPasswordTitle": "Forgot password",
+ "forgotYourPassword": "forgot your password",
+ "goToAuth": "Go to auth",
+ "invalidPassword": "You have entered wrong account password.",
+ "keyNotExists": "The key is incorrect",
+ "keyRequired": "Please, enter an activation key",
+ "loginNotExist": "Sorry, Ely doesn't recognise your login.",
+ "loginRequired": "Please enter email or username",
+ "loginTitle": "Sign in",
+ "logout": "Logout",
+ "mojangPriorityWarning": "A Mojang account with the same nickname was found. According to project rules, account owner has the right to demand the restoration of control over nickname.",
+ "newEmailPlaceholder": "Enter new E-mail",
+ "newPassword": "Enter new password",
+ "newPasswordRequired": "Please enter new password",
+ "newRePassword": "Repeat new password",
+ "newRePasswordRequired": "Please repeat new password",
+ "next": "Next",
+ "nickname": "Nickname",
+ "oldHashingAlgoWarning": "Your was hashed with an old hashing algorithm.
Please, change password.",
+ "passCodeToApp": "To complete authorization process, please, provide the following code to {appName}",
+ "password": "Password",
+ "passwordChangeMessage": "To enhance the security of your account, please change your password.",
+ "passwordRequired": "Please enter password",
+ "passwordTooShort": "Your password is too short",
+ "passwordsDoesNotMatch": "The passwords does not match",
+ "permissionsTitle": "Application permissions",
+ "personalData": "Personal data",
+ "pleaseEnterPassword": "Please, enter your current password",
+ "preferencesDescription": "Here you can change the key preferences of your account. Please note that all actions must be confirmed by entering a password.",
+ "pressButtonToStart": "Press the button below to send a message with the code for E-mail change initialization.",
+ "rePasswordRequired": "Please retype your password",
+ "register": "Join",
+ "registerTitle": "Sign Up",
+ "repeatPassword": "Repeat password",
+ "rulesAgreementRequired": "You must accept rules in order to create an account",
+ "scope_minecraft_server_session": "Authorization data for minecraft server",
+ "scope_offline_access": "Access to your profile data, when you offline",
+ "sendEmailButton": "Send E-mail",
+ "sendMail": "Send mail",
+ "signUpButton": "Register",
+ "skipThisStep": "Skip password changing",
+ "suggestResetPassword": "Are you have {link}?",
+ "termsOfService": "Terms of service",
+ "theAppNeedsAccess1": "This application needs access",
+ "theAppNeedsAccess2": "to your data",
+ "title": "Confirm your action",
+ "twoFactorAuth": "Two factor auth",
+ "usernameRequired": "Username is required",
+ "usernameUnavailable": "This username is already taken",
+ "waitAppReaction": "Please, wait till your application response",
+ "youAuthorizedAs": "You authorized as:",
+ "yourEmail": "Your E-mail",
+ "yourNickname": "Your nickname"
+}
diff --git a/src/index.js b/src/index.js
index 78a18b6..b070e5c 100644
--- a/src/index.js
+++ b/src/index.js
@@ -15,7 +15,9 @@ import thunk from 'redux-thunk';
import { Router, browserHistory } from 'react-router';
import { syncHistory, routeReducer } from 'react-router-redux';
-import { IntlProvider } from 'react-intl';
+import { IntlProvider, addLocaleData } from 'react-intl';
+import enLocaleData from 'react-intl/locale-data/en';
+import ruLocaleData from 'react-intl/locale-data/ru';
import reducers from 'reducers';
import routesFactory from 'routes';
@@ -32,23 +34,47 @@ const store = applyMiddleware(
thunk
)(createStore)(reducer);
-if (process.env.NODE_ENV !== 'production') {
- // some shortcuts for testing on localhost
+addLocaleData(enLocaleData);
+addLocaleData(ruLocaleData);
- window.testOAuth = () => location.href = '/oauth?client_id=ely&redirect_uri=http%3A%2F%2Fely.by&response_type=code&scope=minecraft_server_session';
+// TODO: bind with user state
+const SUPPORTED_LANGUAGES = ['ru', 'en'];
+const DEFAULT_LANGUAGE = 'en';
+const state = store.getState();
+function getUserLanguages() {
+ return [].concat(state.user.lang || [])
+ .concat(navigator.languages || [])
+ .concat(navigator.language || []);
}
-ReactDOM.render(
-
-
-
- {routesFactory(store)}
-
-
- ,
- document.getElementById('app')
-);
+function detectLanguage(userLanguages, availableLanguages, defaultLanguage) {
+ return (userLanguages || [])
+ .concat(defaultLanguage)
+ .map((lang) => lang.split('-').shift().toLowerCase())
+ .find((lang) => availableLanguages.indexOf(lang) !== -1);
+}
-setTimeout(() => {
- document.getElementById('loader').classList.remove('is-active');
-}, 50);
+const locale = detectLanguage(getUserLanguages(), SUPPORTED_LANGUAGES, DEFAULT_LANGUAGE);
+
+new Promise(require(`bundle!i18n/${locale}.json`))
+ .then((messages) => {
+ ReactDOM.render(
+
+
+
+ {routesFactory(store)}
+
+
+ ,
+ document.getElementById('app')
+ );
+
+ document.getElementById('loader').classList.remove('is-active');
+ });
+
+
+
+if (process.env.NODE_ENV !== 'production') {
+ // some shortcuts for testing on localhost
+ window.testOAuth = () => location.href = '/oauth?client_id=ely&redirect_uri=http%3A%2F%2Fely.by&response_type=code&scope=minecraft_server_session';
+}
diff --git a/webpack.config.js b/webpack.config.js
index 2406c45..d477605 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -156,15 +156,26 @@ var webpackConfig = {
},
{
test: /\.json$/,
+ exclude: /intl\.json/,
loader: 'json'
},
{
test: /\.html$/,
loader: 'html'
+ },
+ {
+ test: /\.intl\.json$/,
+ loader: 'babel!intl-loader!json'
}
]
},
+ resolveLoader: {
+ alias: {
+ 'intl-loader': path.resolve('./webpack/intl-loader')
+ }
+ },
+
sassLoader: {
importer: iconfontImporter({
test: /\.font.(js|json)$/,
diff --git a/webpack/intl-loader.js b/webpack/intl-loader.js
new file mode 100644
index 0000000..a6c1a59
--- /dev/null
+++ b/webpack/intl-loader.js
@@ -0,0 +1,21 @@
+module.exports = function() {
+ this.cacheable && this.cacheable();
+
+ var moduleId = this.context
+ .replace(this.options.resolve.root, '')
+ .replace(/^\/|\/$/g, '')
+ .replace(/\//g, '.');
+
+ var content = this.inputValue[0];
+ content = JSON.stringify(Object.keys(content).reduce(function(translations, key) {
+ translations[key] = {
+ id: moduleId + '.' + key,
+ defaultMessage: content[key]
+ };
+
+ return translations;
+ }, {}));
+
+ return 'import { defineMessages } from \'react-intl\';'
+ + 'export default defineMessages(' + content + ')';
+};