Merge pull request #25 from elyby/crowdin_rework

Crowdin rework
This commit is contained in:
ErickSkrauch 2020-06-15 00:33:18 +03:00 committed by GitHub
commit 20be130d4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
134 changed files with 1627 additions and 5278 deletions

View File

@ -9,4 +9,4 @@ end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
indent_size = 4

View File

@ -8,7 +8,6 @@ module.exports = {
extends: [
'eslint:recommended',
'plugin:jsdoc/recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'prettier/@typescript-eslint',
'plugin:prettier/recommended',

View File

@ -64,6 +64,8 @@ Yarn:
policy: pull-push
variables:
CYPRESS_INSTALL_BINARY: 0 # Don't install binary to increase caching performance between jobs
before_script:
- apk add git
script:
- yarn install --frozen-lockfile
@ -74,6 +76,8 @@ Yarn E2E:
- .yarnE2ECache
cache:
policy: pull-push
before_script:
- apk add git
script:
- yarn install --frozen-lockfile
only:
@ -114,6 +118,24 @@ Jest:
script:
- yarn test
Crowdin:
stage: test
image: $NODE_IMAGE
needs:
- Yarn
extends:
- .yarnCache
script:
- yarn i18n:extract
- yarn i18n:push
- yarn i18n:pull
artifacts:
name: "i18n for $CI_COMMIT_BRANCH-$CI_COMMIT_SHORT_SHA"
paths:
- packages/app/i18n/*.json
- packages/app/i18n/index.js
expire_in: 1 week
Cypress:
stage: test
image: $NODE_E2E_IMAGE
@ -151,6 +173,7 @@ Build:
- Lint
- TypeScript
- Jest
- Crowdin
extends:
- .yarnCache
before_script:
@ -158,7 +181,6 @@ Build:
script:
- yarn build
# Remove unneeded files
- rm -rf build/messages
- rm -rf build/*.css.map
# Move all source maps to its own directory
- mkdir -p source-maps
@ -179,6 +201,7 @@ Storybook:
image: $NODE_IMAGE
needs:
- Yarn
- Crowdin
extends:
- .yarnCache
script:

View File

@ -1,98 +0,0 @@
declare module 'crowdin-api' {
export interface ProjectInfoFile {
node_type: 'file';
id: number;
name: string;
created: string;
last_updated: string;
last_accessed: string;
last_revision: string;
}
export interface ProjectInfoDirectory {
node_type: 'directory';
id: number;
name: string;
files: Array<ProjectInfoFile | ProjectInfoDirectory>;
}
export interface ProjectInfoResponse {
details: {
source_language: {
name: string;
code: string;
};
name: string;
identifier: string;
created: string;
description: string;
join_policy: string;
last_build: string | null;
last_activity: string;
participants_count: string; // it's number, but string in the response
logo_url: string | null;
total_strings_count: string; // it's number, but string in the response
total_words_count: string; // it's number, but string in the response
duplicate_strings_count: number;
duplicate_words_count: number;
invite_url: {
translator: string;
proofreader: string;
};
};
languages: Array<{
name: string; // English language name
code: string;
can_translate: 0 | 1;
can_approve: 0 | 1;
}>;
files: Array<ProjectInfoFile | ProjectInfoDirectory>;
}
export interface LanguageStatusNode {
node_type: 'directory' | 'file';
id: number;
name: string;
phrases: number;
translated: number;
approved: number;
words: number;
words_translated: number;
words_approved: number;
files: Array<LanguageStatusNode>;
}
export interface LanguageStatusResponse {
files: Array<LanguageStatusNode>;
}
type FilesList = Record<string, string | ReadableStream>;
export default class CrowdinApi {
constructor(params: { apiKey: string; projectName: string; baseUrl?: string });
projectInfo(): Promise<ProjectInfoResponse>;
languageStatus(language: string): Promise<LanguageStatusResponse>;
exportFile(
file: string,
language: string,
params?: {
branch?: string;
format?: 'xliff';
export_translated_only?: boolean;
export_approved_only?: boolean;
},
): Promise<string>; // TODO: not sure about Promise return type
updateFile(
files: FilesList,
params: {
titles?: Record<string, string>;
export_patterns?: Record<string, string>;
new_names?: Record<string, string>;
first_line_contains_header?: string;
scheme?: string;
update_option?: 'update_as_unapproved' | 'update_without_changes';
branch?: string;
},
): Promise<void>;
}
}

View File

@ -1,10 +0,0 @@
declare module 'multi-progress' {
export default class MultiProgress {
constructor(stream?: string);
newBar(schema: string, options: ProgressBar.ProgressBarOptions): ProgressBar;
terminate(): void;
move(index: number): void;
tick(index: number, value?: number, options?: any): void;
update(index: number, value?: number, options?: any): void;
}
}

56
@types/prompt.d.ts vendored
View File

@ -1,56 +0,0 @@
// Type definitions for Prompt.js 1.0.0
// Project: https://github.com/flatiron/prompt
declare module 'prompt' {
type PropertiesType = Array<string> | prompt.PromptSchema | Array<prompt.PromptPropertyOptions>;
namespace prompt {
interface PromptSchema {
properties: PromptProperties;
}
interface PromptProperties {
[propName: string]: PromptPropertyOptions;
}
interface PromptPropertyOptions {
name?: string;
pattern?: RegExp;
message?: string;
required?: boolean;
hidden?: boolean;
description?: string;
type?: string;
default?: string;
before?: (value: any) => any;
conform?: (result: any) => boolean;
}
export function start(): void;
export function get<T extends PropertiesType>(
properties: T,
callback: (
err: Error,
result: T extends Array<string>
? Record<T[number], string>
: T extends PromptSchema
? Record<keyof T['properties'], string>
: T extends Array<PromptPropertyOptions>
? Record<T[number]['name'] extends string ? T[number]['name'] : number, string>
: never,
) => void,
): void;
export function addProperties(obj: any, properties: PropertiesType, callback: (err: Error) => void): void;
export function history(propertyName: string): any;
export let override: any;
export let colors: boolean;
export let message: string;
export let delimiter: string;
}
export = prompt;
}

24
@types/react-intl.d.ts vendored Normal file
View File

@ -0,0 +1,24 @@
import { PrimitiveType } from 'intl-messageformat';
import { Formatters, IntlConfig, IntlFormatters, MessageDescriptor } from 'react-intl';
declare module 'react-intl' {
// Babel's plugin react-intl-auto always converts passed strings messages into the MessageDescriptor
export declare function defineMessages<U extends Record<string, string | MessageDescriptor>>(
msgs: U,
): Record<keyof U, MessageDescriptor>;
// Babel's plugin react-intl-auto allows to call the formatMessage function with "key" field to automatically
// generate message's id
export interface KeyBasedMessageDescriptor {
key: string;
defaultMessage: string;
}
export declare interface IntlShape extends IntlConfig, IntlFormatters {
formatters: Formatters;
formatMessage(
descriptor: MessageDescriptor | KeyBasedMessageDescriptor,
values?: Record<string, PrimitiveType>,
): string;
}
}

View File

@ -1,40 +1,64 @@
/* eslint-env node */
module.exports = {
presets: [
[
'@babel/preset-typescript',
{
allowDeclareFields: true,
},
],
'@babel/preset-react',
'@babel/preset-env',
],
plugins: [
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-proposal-function-bind',
'@babel/plugin-proposal-class-properties',
[
'@babel/plugin-transform-runtime',
{
corejs: 3,
},
],
['react-intl', { messagesDir: './build/messages/' }],
],
env: {
webpack: {
plugins: ['react-hot-loader/babel'],
presets: [
[
'@babel/preset-env',
{
modules: false,
useBuiltIns: 'usage', // or "entry"
corejs: 3,
},
],
// @ts-nocheck
module.exports = function (api) {
const env = api.env();
api.cache(true);
return {
presets: [
[
'@babel/preset-typescript',
{
allowDeclareFields: true,
},
],
'@babel/preset-react',
[
'@babel/preset-env',
{
ignoreBrowserslistConfig: true,
targets: {
node: true,
},
modules: 'commonjs',
},
],
],
plugins: [
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-proposal-function-bind',
'@babel/plugin-proposal-class-properties',
[
'@babel/plugin-transform-runtime',
{
corejs: 3,
},
],
[
'react-intl-auto',
{
removePrefix: 'packages.app',
messagesDir: './build/messages/',
useKey: true,
removeDefaultMessage: env === 'production',
},
],
],
env: {
webpack: {
plugins: ['react-hot-loader/babel'],
presets: [
[
'@babel/preset-env',
{
ignoreBrowserslistConfig: false,
modules: false,
useBuiltIns: 'usage', // or "entry"
corejs: 3,
},
],
],
},
},
},
};
};

View File

@ -1,4 +1,5 @@
/* eslint-env node */
/* eslint-disable @typescript-eslint/no-var-requires */
require('dotenv').config();
@ -9,5 +10,12 @@ module.exports = {
apiHost: env.API_HOST || 'https://dev.account.ely.by',
ga: env.GA_ID && { id: env.GA_ID },
sentryDSN: env.SENTRY_DSN,
crowdinApiKey: env.CROWDIN_API_KEY,
crowdin: {
apiKey: env.CROWDIN_API_KEY,
projectId: 350687,
filePath: 'accounts/site.json',
sourceLang: 'en',
basePath: `${__dirname}/packages/app/i18n`,
minApproved: 80, // Minimal ready percent before translation can be published
},
};

View File

@ -35,7 +35,7 @@
"ts:check": "tsc",
"ci:check": "yarn lint:check && yarn ts:check && yarn test",
"analyze": "yarn run clean && yarn run build:webpack --analyze",
"i18n:collect": "babel-node --extensions \".ts\" ./packages/scripts/i18n-collect.ts",
"i18n:extract": "extract-messages -l=en -o ./packages/app/i18n --flat true 'packages/app/!(node_modules)/**/*.[tj]s?(x)'",
"i18n:push": "babel-node --extensions \".ts\" ./packages/scripts/i18n-crowdin.ts push",
"i18n:pull": "babel-node --extensions \".ts\" ./packages/scripts/i18n-crowdin.ts pull",
"build": "yarn run clean && yarn run build:webpack",
@ -73,7 +73,6 @@
"\\.(css|less|scss)$": "identity-obj-proxy"
},
"transform": {
"\\.intl\\.json$": "<rootDir>/jest/__mocks__/intlMock.js",
"^.+\\.[tj]sx?$": "babel-jest"
}
},
@ -117,7 +116,7 @@
"@typescript-eslint/eslint-plugin": "^3.0.0",
"@typescript-eslint/parser": "^3.0.0",
"babel-loader": "^8.0.0",
"babel-plugin-react-intl": "^7.5.10",
"babel-plugin-react-intl-auto": "https://github.com/elyby/babel-plugin-react-intl-auto.git#build",
"core-js": "3.6.5",
"csp-webpack-plugin": "^2.0.2",
"css-loader": "^3.5.3",
@ -130,6 +129,7 @@
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-react": "^7.20.0",
"exports-loader": "^0.7.0",
"extract-react-intl-messages": "^4.1.1",
"file-loader": "^6.0.0",
"html-loader": "^1.1.0",
"html-webpack-plugin": "^4.3.0",

View File

@ -1,5 +0,0 @@
{
"addAccount": "Add account",
"goToEly": "Go to Ely.by profile",
"logout": "Log out"
}

View File

@ -11,7 +11,6 @@ import { getActiveAccount, Account } from 'app/components/accounts/reducer';
import { RootState } from 'app/reducers';
import styles from './accountSwitcher.scss';
import messages from './AccountSwitcher.intl.json';
interface Props {
switchAccount: (account: Account) => Promise<Account>;
@ -70,7 +69,7 @@ export class AccountSwitcher extends React.Component<Props> {
<div className={styles.links}>
<div className={styles.link}>
<a href={`http://ely.by/u${activeAccount.id}`} target="_blank">
<Message {...messages.goToEly} />
<Message key="goToEly" defaultMessage="Go to Ely.by profile" />
</a>
</div>
<div className={styles.link}>
@ -80,7 +79,7 @@ export class AccountSwitcher extends React.Component<Props> {
onClick={this.onRemove(activeAccount)}
href="#"
>
<Message {...messages.logout} />
<Message key="logout" defaultMessage="Log out" />
</a>
</div>
</div>
@ -127,7 +126,7 @@ export class AccountSwitcher extends React.Component<Props> {
small
className={styles.addAccount}
label={
<Message {...messages.addAccount}>
<Message key="addAccount" defaultMessage="Add account">
{(message) => (
<span>
<div className={styles.addIcon} />

View File

@ -12,6 +12,23 @@ import { Dispatch, RootState } from 'app/reducers';
import { Account } from './reducer';
jest.mock('app/i18n', () => ({
en: {
code: 'en',
name: 'English',
englishName: 'English',
progress: 100,
isReleased: true,
},
be: {
code: 'be',
name: 'Беларуская',
englishName: 'Belarusian',
progress: 97,
isReleased: true,
},
}));
const token = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJlbHl8MSJ9.pRJ7vakt2eIscjqwG__KhSxKb3qwGsdBBeDbBffJs_I';
const legacyToken = 'eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOjF9.cRF-sQNrwWQ94xCb3vWioVdjxAZeefEE7GMGwh7708o';

View File

@ -1,8 +0,0 @@
{
"title": "User Agreement",
"accept": "Accept",
"declineAndLogout": "Decline and logout",
"description1": "We have updated our {link}.",
"termsOfService": "terms of service",
"description2": "In order to continue using {name} service, you need to accept them."
}

View File

@ -1,6 +1,12 @@
import { defineMessages } from 'react-intl';
import factory from '../factory';
import Body from './AcceptRulesBody';
import messages from './AcceptRules.intl.json';
const messages = defineMessages({
title: 'User Agreement',
accept: 'Accept',
declineAndLogout: 'Decline and logout',
});
export default factory({
title: messages.title,

View File

@ -5,10 +5,9 @@ import { Link } from 'react-router-dom';
import icons from 'app/components/ui/icons.scss';
import BaseAuthBody from 'app/components/auth/BaseAuthBody';
import appInfo from 'app/components/auth/appInfo/AppInfo.intl.json';
import appName from 'app/components/auth/appInfo/appName.intl';
import styles from './acceptRules.scss';
import messages from './AcceptRules.intl.json';
export default class AcceptRulesBody extends BaseAuthBody {
static displayName = 'AcceptRulesBody';
@ -25,20 +24,22 @@ export default class AcceptRulesBody extends BaseAuthBody {
<p className={styles.descriptionText}>
<Message
{...messages.description1}
key="description1"
defaultMessage="We have updated our {link}."
values={{
link: (
<Link to="/rules" target="_blank">
<Message {...messages.termsOfService} />
<Message key="termsOfService" defaultMessage="terms of service" />
</Link>
),
}}
/>
<br />
<Message
{...messages.description2}
key="description2"
defaultMessage="In order to continue using {name} service, you need to accept them."
values={{
name: <Message {...appInfo.appName} />,
name: <Message {...appName} />,
}}
/>
</p>

View File

@ -1,8 +0,0 @@
{
"accountActivationTitle": "Account activation",
"activationMailWasSent": "Please check {email} for the message with further instructions",
"activationMailWasSentNoEmail": "Please check your Email for the message with further instructions",
"confirmEmail": "Confirm Email",
"didNotReceivedEmail": "Did not received Email?",
"enterTheCode": "Enter the code from Email here"
}

View File

@ -1,7 +1,13 @@
import { defineMessages } from 'react-intl';
import factory from '../factory';
import messages from './Activation.intl.json';
import Body from './ActivationBody';
const messages = defineMessages({
accountActivationTitle: 'Account activation',
confirmEmail: 'Confirm Email',
didNotReceivedEmail: 'Did not received Email?',
});
export default factory({
title: messages.accountActivationTitle,
body: Body,

View File

@ -1,12 +1,15 @@
import React from 'react';
import { FormattedMessage as Message } from 'react-intl';
import { defineMessages, FormattedMessage as Message } from 'react-intl';
import { Input } from 'app/components/ui/form';
import BaseAuthBody from 'app/components/auth/BaseAuthBody';
import styles from './activation.scss';
import messages from './Activation.intl.json';
const messages = defineMessages({
enterTheCode: 'Enter the code from Email here',
});
export default class ActivationBody extends BaseAuthBody {
static displayName = 'ActivationBody';
@ -28,13 +31,17 @@ export default class ActivationBody extends BaseAuthBody {
<div className={styles.descriptionText}>
{email ? (
<Message
{...messages.activationMailWasSent}
key="activationMailWasSent"
defaultMessage="Please check {email} for the message with further instructions"
values={{
email: <b>{email}</b>,
}}
/>
) : (
<Message {...messages.activationMailWasSentNoEmail} />
<Message
key="activationMailWasSentNoEmail"
defaultMessage="Please check your Email for the message with further instructions"
/>
)}
</div>
</div>

View File

@ -1,7 +0,0 @@
{
"appName": "Ely Accounts",
"goToAuth": "Go to auth",
"appDescription": "You are on the Ely.by authorization service, that allows you to safely perform any operations on your account. This single entry point for websites and desktop software, including game launchers.",
"useItYourself": "Visit our {link}, to learn how to use this service in you projects.",
"documentation": "documentation"
}

View File

@ -1,10 +1,14 @@
import React from 'react';
import { FormattedMessage as Message } from 'react-intl';
import { defineMessages, FormattedMessage as Message } from 'react-intl';
import { Button } from 'app/components/ui/form';
import { FooterMenu } from 'app/components/footerMenu';
import appName from './appName.intl';
import styles from './appInfo.scss';
import messages from './AppInfo.intl.json';
const messages = defineMessages({
goToAuth: 'Go to auth',
});
export default class AppInfo extends React.Component<{
name?: string;
@ -17,7 +21,7 @@ export default class AppInfo extends React.Component<{
return (
<div className={styles.appInfo}>
<div className={styles.logoContainer}>
<h2 className={styles.logo}>{name ? name : <Message {...messages.appName} />}</h2>
<h2 className={styles.logo}>{name ? name : <Message {...appName} />}</h2>
</div>
<div className={styles.descriptionContainer}>
{description ? (
@ -25,15 +29,19 @@ export default class AppInfo extends React.Component<{
) : (
<div>
<p className={styles.description}>
<Message {...messages.appDescription} />
<Message
key="appDescription"
defaultMessage="You are on the Ely.by authorization service, that allows you to safely perform any operations on your account. This single entry point for websites and desktop software, including game launchers."
/>
</p>
<p className={styles.description}>
<Message
{...messages.useItYourself}
key="useItYourself"
defaultMessage="Visit our {link}, to learn how to use this service in you projects."
values={{
link: (
<a href="http://docs.ely.by/oauth.html">
<Message {...messages.documentation} />
<Message key="documentation" defaultMessage="documentation" />
</a>
),
}}

View File

@ -0,0 +1,7 @@
import { defineMessages } from 'react-intl';
const { appName } = defineMessages({
appName: 'Ely Accounts',
});
export default appName;

View File

@ -1,7 +0,0 @@
{
"chooseAccountTitle": "Choose an account",
"addAccount": "Log into another account",
"logoutAll": "Log out from all accounts",
"pleaseChooseAccount": "Please select an account you're willing to use",
"pleaseChooseAccountForApp": "Please select an account that you want to use to authorize {appName}"
}

View File

@ -1,7 +1,13 @@
import { defineMessages } from 'react-intl';
import factory from '../factory';
import messages from './ChooseAccount.intl.json';
import Body from './ChooseAccountBody';
const messages = defineMessages({
chooseAccountTitle: 'Choose an account',
addAccount: 'Log into another account',
logoutAll: 'Log out from all accounts',
});
export default factory({
title: messages.chooseAccountTitle,
body: Body,

View File

@ -7,7 +7,6 @@ import { AccountSwitcher } from 'app/components/accounts';
import { Account } from 'app/components/accounts/reducer';
import styles from './chooseAccount.scss';
import messages from './ChooseAccount.intl.json';
export default class ChooseAccountBody extends BaseAuthBody {
static displayName = 'ChooseAccountBody';
@ -23,14 +22,18 @@ export default class ChooseAccountBody extends BaseAuthBody {
<div className={styles.description}>
{client ? (
<Message
{...messages.pleaseChooseAccountForApp}
key="pleaseChooseAccountForApp"
defaultMessage="Please select an account that you want to use to authorize {appName}"
values={{
appName: <span className={styles.appName}>{client.name}</span>,
}}
/>
) : (
<div className={styles.description}>
<Message {...messages.pleaseChooseAccount} />
<Message
key="pleaseChooseAccount"
defaultMessage="Please select an account you're willing to use"
/>
</div>
)}
</div>

View File

@ -1,7 +0,0 @@
{
"authForAppSuccessful": "Authorization for {appName} was successfully completed",
"authForAppFailed": "Authorization for {appName} was failed",
"waitAppReaction": "Please, wait till your application response",
"passCodeToApp": "To complete authorization process, please, provide the following code to {appName}",
"copy": "Copy"
}

View File

@ -1,14 +1,17 @@
import React, { MouseEventHandler } from 'react';
import { connect } from 'react-redux';
import { FormattedMessage as Message } from 'react-intl';
import { defineMessages, FormattedMessage as Message } from 'react-intl';
import { Helmet } from 'react-helmet-async';
import { Button } from 'app/components/ui/form';
import copy from 'app/services/copy';
import { RootState } from 'app/reducers';
import messages from './Finish.intl.json';
import styles from './finish.scss';
const messages = defineMessages({
copy: 'Copy',
});
interface Props {
appName: string;
code?: string;
@ -36,7 +39,8 @@ class Finish extends React.Component<Props> {
<div className={styles.successBackground} />
<div className={styles.greenTitle}>
<Message
{...messages.authForAppSuccessful}
key="authForAppSuccessful"
defaultMessage="Authorization for {appName} was successfully completed"
values={{
appName: <span className={styles.appName}>{appName}</span>,
}}
@ -45,7 +49,11 @@ class Finish extends React.Component<Props> {
{displayCode ? (
<div data-testid="oauth-code-container">
<div className={styles.description}>
<Message {...messages.passCodeToApp} values={{ appName }} />
<Message
key="passCodeToApp"
defaultMessage="To complete authorization process, please, provide the following code to {appName}"
values={{ appName }}
/>
</div>
<div className={styles.codeContainer}>
<div className={styles.code}>{code}</div>
@ -54,7 +62,10 @@ class Finish extends React.Component<Props> {
</div>
) : (
<div className={styles.description}>
<Message {...messages.waitAppReaction} />
<Message
key="waitAppReaction"
defaultMessage="Please, wait till your application response"
/>
</div>
)}
</div>
@ -63,14 +74,18 @@ class Finish extends React.Component<Props> {
<div className={styles.failBackground} />
<div className={styles.redTitle}>
<Message
{...messages.authForAppFailed}
key="authForAppFailed"
defaultMessage="Authorization for {appName} was failed"
values={{
appName: <span className={styles.appName}>{appName}</span>,
}}
/>
</div>
<div className={styles.description}>
<Message {...messages.waitAppReaction} />
<Message
key="waitAppReaction"
defaultMessage="Please, wait till your application response"
/>
</div>
</div>
)}

View File

@ -1,7 +0,0 @@
{
"title": "Forgot password",
"sendMail": "Send mail",
"specifyEmail": "Specify the registration Email address or last used username for your account and we will send an Email with instructions for further password recovery.",
"pleasePressButton": "Please press the button bellow to get an Email with password recovery code.",
"alreadyHaveCode": "Already have a code"
}

View File

@ -1,7 +1,13 @@
import { defineMessages } from 'react-intl';
import factory from '../factory';
import messages from './ForgotPassword.intl.json';
import Body from './ForgotPasswordBody';
const messages = defineMessages({
title: 'Forgot password',
sendMail: 'Send mail',
alreadyHaveCode: 'Already have a code',
});
export default factory({
title: messages.title,
body: Body,

View File

@ -1,6 +1,6 @@
import React from 'react';
import { FormattedMessage as Message } from 'react-intl';
import { defineMessages, FormattedMessage as Message } from 'react-intl';
import { Input, Captcha } from 'app/components/ui/form';
import { getLogin } from 'app/components/auth/reducer';
@ -8,7 +8,10 @@ import { PanelIcon } from 'app/components/ui/Panel';
import BaseAuthBody from 'app/components/auth/BaseAuthBody';
import styles from './forgotPassword.scss';
import messages from './ForgotPassword.intl.json';
const messages = defineMessages({
emailOrUsername: 'Email or username',
});
export default class ForgotPasswordBody extends BaseAuthBody {
static displayName = 'ForgotPasswordBody';
@ -36,14 +39,17 @@ export default class ForgotPasswordBody extends BaseAuthBody {
{isLoginEditShown ? (
<div>
<p className={styles.descriptionText}>
<Message {...messages.specifyEmail} />
<Message
key="specifyEmail"
defaultMessage="Specify the registration Email address or last used username for your account and we will send an Email with instructions for further password recovery."
/>
</p>
<Input
{...this.bindField('login')}
icon="envelope"
color="lightViolet"
required
placeholder={messages.accountEmail}
placeholder={messages.emailOrUsername}
defaultValue={login}
/>
</div>
@ -54,7 +60,10 @@ export default class ForgotPasswordBody extends BaseAuthBody {
<span className={styles.editLogin} onClick={this.onClickEdit} data-testid="edit-login" />
</div>
<p className={styles.descriptionText}>
<Message {...messages.pleasePressButton} />
<Message
key="pleasePressButton"
defaultMessage="Please press the button bellow to get an Email with password recovery code."
/>
</p>
</div>
)}

View File

@ -1,6 +0,0 @@
{
"createNewAccount": "Create new account",
"loginTitle": "Sign in",
"emailOrUsername": "Email or username",
"next": "Next"
}

View File

@ -1,6 +1,12 @@
import { defineMessages } from 'react-intl';
import factory from '../factory';
import Body from './LoginBody';
import messages from './Login.intl.json';
const messages = defineMessages({
createNewAccount: 'Create new account',
loginTitle: 'Sign in',
next: 'Next',
});
export default factory({
title: messages.loginTitle,

View File

@ -1,10 +1,13 @@
import React from 'react';
import { defineMessages } from 'react-intl';
import { Input } from 'app/components/ui/form';
import BaseAuthBody from 'app/components/auth/BaseAuthBody';
import { User } from 'app/components/user/reducer';
import messages from './Login.intl.json';
const messages = defineMessages({
emailOrUsername: 'Email or username',
});
export default class LoginBody extends BaseAuthBody {
static displayName = 'LoginBody';

View File

@ -1,4 +0,0 @@
{
"enterTotp": "Enter code",
"description": "In order to sign in this account, you need to enter a one-time password from mobile application"
}

View File

@ -1,13 +1,17 @@
import { defineMessages } from 'react-intl';
import factory from '../factory';
import Body from './MfaBody';
import messages from './Mfa.intl.json';
import passwordMessages from '../password/Password.intl.json';
const messages = defineMessages({
enterTotp: 'Enter code',
signInButton: 'Sign in',
});
export default factory({
title: messages.enterTotp,
body: Body,
footer: {
color: 'green',
label: passwordMessages.signInButton,
label: messages.signInButton,
},
});

View File

@ -1,11 +1,14 @@
import React from 'react';
import { FormattedMessage as Message } from 'react-intl';
import { defineMessages, FormattedMessage as Message } from 'react-intl';
import { PanelIcon } from 'app/components/ui/Panel';
import { Input } from 'app/components/ui/form';
import BaseAuthBody from 'app/components/auth/BaseAuthBody';
import styles from './mfa.scss';
import messages from './Mfa.intl.json';
const messages = defineMessages({
enterTotp: 'Enter code',
});
export default class MfaBody extends BaseAuthBody {
static panelId = 'mfa';
@ -21,7 +24,10 @@ export default class MfaBody extends BaseAuthBody {
<PanelIcon icon="lock" />
<p className={styles.descriptionText}>
<Message {...messages.description} />
<Message
key="description"
defaultMessage="In order to sign in this account, you need to enter a one-time password from mobile application"
/>
</p>
<Input

View File

@ -1,7 +0,0 @@
{
"passwordTitle": "Enter password",
"signInButton": "Sign in",
"forgotPassword": "Forgot password",
"accountPassword": "Account password",
"rememberMe": "Remember me on this device"
}

View File

@ -1,6 +1,12 @@
import { defineMessages } from 'react-intl';
import factory from '../factory';
import Body from './PasswordBody';
import messages from './Password.intl.json';
const messages = defineMessages({
passwordTitle: 'Enter password',
signInButton: 'Sign in',
forgotPassword: 'Forgot password',
});
export default factory({
title: messages.passwordTitle,

View File

@ -3,9 +3,14 @@ import icons from 'app/components/ui/icons.scss';
import { Input, Checkbox } from 'app/components/ui/form';
import BaseAuthBody from 'app/components/auth/BaseAuthBody';
import authStyles from 'app/components/auth/auth.scss';
import { defineMessages } from 'react-intl';
import styles from './password.scss';
import messages from './Password.intl.json';
const messages = defineMessages({
accountPassword: 'Account password',
rememberMe: 'Remember me on this device',
});
export default class PasswordBody extends BaseAuthBody {
static displayName = 'PasswordBody';

View File

@ -1,12 +0,0 @@
{
"permissionsTitle": "Application permissions",
"youAuthorizedAs": "You authorized as:",
"theAppNeedsAccess1": "This application needs access",
"theAppNeedsAccess2": "to your data",
"decline": "Decline",
"approve": "Approve",
"scope_minecraft_server_session": "Authorization data for minecraft server",
"scope_offline_access": "Access to your profile data, when you offline",
"scope_account_info": "Access to your profile data (except Email)",
"scope_account_email": "Access to your Email address"
}

View File

@ -1,7 +1,13 @@
import { defineMessages } from 'react-intl';
import factory from '../factory';
import messages from './Permissions.intl.json';
import Body from './PermissionsBody';
const messages = defineMessages({
permissionsTitle: 'Application permissions',
decline: 'Decline',
approve: 'Approve',
});
export default factory({
title: messages.permissionsTitle,
body: Body,

View File

@ -1,11 +1,17 @@
import React from 'react';
import { FormattedMessage as Message } from 'react-intl';
import { defineMessages, FormattedMessage as Message } from 'react-intl';
import icons from 'app/components/ui/icons.scss';
import { PanelBodyHeader } from 'app/components/ui/Panel';
import BaseAuthBody from 'app/components/auth/BaseAuthBody';
import styles from './permissions.scss';
import messages from './Permissions.intl.json';
const scopesMessages = defineMessages({
scope_minecraft_server_session: 'Authorization data for minecraft server',
scope_offline_access: 'Access to your profile data, when you offline',
scope_account_info: 'Access to your profile data (except Email)',
scope_account_email: 'Access to your Email address',
});
export default class PermissionsBody extends BaseAuthBody {
static displayName = 'PermissionsBody';
@ -25,21 +31,22 @@ export default class PermissionsBody extends BaseAuthBody {
{user.avatar ? <img src={user.avatar} /> : <span className={icons.user} />}
</div>
<div className={styles.authInfoTitle}>
<Message {...messages.youAuthorizedAs} />
<Message key="youAuthorizedAs" defaultMessage="You authorized as:" />
</div>
<div className={styles.authInfoEmail}>{user.username}</div>
</div>
</PanelBodyHeader>
<div className={styles.permissionsContainer}>
<div className={styles.permissionsTitle}>
<Message {...messages.theAppNeedsAccess1} />
<Message key="theAppNeedsAccess1" defaultMessage="This application needs access" />
<br />
<Message {...messages.theAppNeedsAccess2} />
<Message key="theAppNeedsAccess2" defaultMessage="to your data" />
</div>
<ul className={styles.permissionsList}>
{scopes.map((scope) => {
const key = `scope_${scope}`;
const message = messages[key];
// @ts-ignore
const message = scopesMessages[key];
return (
<li key={key}>

View File

@ -1,12 +0,0 @@
{
"title": "Restore password",
"contactSupport": "Contact support",
"messageWasSent": "The recovery code was sent to your account Email.",
"messageWasSentTo": "The recovery code was sent to your Email {email}.",
"enterCodeBelow": "Please enter the code received into the field below:",
"enterNewPasswordBelow": "Enter and repeat new password below:",
"change": "Change password",
"newPassword": "Enter new password",
"newRePassword": "Repeat new password",
"enterTheCode": "Enter confirmation code"
}

View File

@ -1,7 +1,13 @@
import { defineMessages } from 'react-intl';
import factory from '../factory';
import messages from './RecoverPassword.intl.json';
import Body from './RecoverPasswordBody';
const messages = defineMessages({
title: 'Restore password',
contactSupport: 'Contact support',
change: 'Change password',
});
export default factory({
title: messages.title,
body: Body,

View File

@ -1,15 +1,20 @@
import React from 'react';
import { FormattedMessage as Message } from 'react-intl';
import { defineMessages, FormattedMessage as Message } from 'react-intl';
import { Input } from 'app/components/ui/form';
import BaseAuthBody from 'app/components/auth/BaseAuthBody';
import styles from './recoverPassword.scss';
import messages from './RecoverPassword.intl.json';
// TODO: activation code field may be decoupled into common component and reused here and in activation panel
const placeholders = defineMessages({
newPassword: 'Enter new password',
newRePassword: 'Repeat new password',
enterTheCode: 'Enter confirmation code',
});
export default class RecoverPasswordBody extends BaseAuthBody {
static displayName = 'RecoverPasswordBody';
static panelId = 'recoverPassword';
@ -28,15 +33,22 @@ export default class RecoverPasswordBody extends BaseAuthBody {
<p className={styles.descriptionText}>
{user.maskedEmail ? (
<Message
{...messages.messageWasSentTo}
key="messageWasSentTo"
defaultMessage="The recovery code was sent to your Email {email}."
values={{
email: <b>{user.maskedEmail}</b>,
}}
/>
) : (
<Message {...messages.messageWasSent} />
<Message
key="messageWasSent"
defaultMessage="The recovery code was sent to your account Email."
/>
)}{' '}
<Message {...messages.enterCodeBelow} />
<Message
key="enterCodeBelow"
defaultMessage="Please enter the code received into the field below:"
/>
</p>
<Input
@ -47,11 +59,11 @@ export default class RecoverPasswordBody extends BaseAuthBody {
value={key}
readOnly={!!key}
autoComplete="off"
placeholder={messages.enterTheCode}
placeholder={placeholders.enterTheCode}
/>
<p className={styles.descriptionText}>
<Message {...messages.enterNewPasswordBelow} />
<Message key="enterNewPasswordBelow" defaultMessage="Enter and repeat new password below:" />
</p>
<Input
@ -60,7 +72,7 @@ export default class RecoverPasswordBody extends BaseAuthBody {
color="lightViolet"
type="password"
required
placeholder={messages.newPassword}
placeholder={placeholders.newPassword}
/>
<Input
@ -69,7 +81,7 @@ export default class RecoverPasswordBody extends BaseAuthBody {
color="lightViolet"
type="password"
required
placeholder={messages.newRePassword}
placeholder={placeholders.newRePassword}
/>
</div>
);

View File

@ -1,10 +0,0 @@
{
"registerTitle": "Sign Up",
"yourNickname": "Your nickname",
"yourEmail": "Your Email",
"accountPassword": "Account password",
"repeatPassword": "Repeat password",
"signUpButton": "Register",
"acceptRules": "I agree with {link}",
"termsOfService": "terms of service"
}

View File

@ -1,9 +1,14 @@
import { defineMessages } from 'react-intl';
import factory from '../factory';
import activationMessages from '../activation/Activation.intl.json';
import forgotPasswordMessages from '../forgotPassword/ForgotPassword.intl.json';
import messages from './Register.intl.json';
import Body from './RegisterBody';
const messages = defineMessages({
registerTitle: 'Sign Up',
signUpButton: 'Register',
didNotReceivedEmail: 'Did not received Email?',
alreadyHaveCode: 'Already have a code',
});
export default factory({
title: messages.registerTitle,
body: Body,
@ -13,11 +18,11 @@ export default factory({
},
links: [
{
label: activationMessages.didNotReceivedEmail,
label: messages.didNotReceivedEmail,
payload: { requestEmail: true },
},
{
label: forgotPasswordMessages.alreadyHaveCode,
label: messages.alreadyHaveCode,
},
],
});

View File

@ -1,15 +1,20 @@
import React from 'react';
import { FormattedMessage as Message } from 'react-intl';
import { defineMessages, FormattedMessage as Message } from 'react-intl';
import { Link } from 'react-router-dom';
import { Input, Checkbox, Captcha } from 'app/components/ui/form';
import BaseAuthBody from 'app/components/auth/BaseAuthBody';
import passwordMessages from '../password/Password.intl.json';
import styles from '../auth.scss';
import messages from './Register.intl.json';
// TODO: password and username can be validate for length and sameness
const placeholders = defineMessages({
yourNickname: 'Your nickname',
yourEmail: 'Your Email',
accountPassword: 'Account password',
repeatPassword: 'Repeat password',
});
export default class RegisterBody extends BaseAuthBody {
static panelId = 'register';
@ -26,7 +31,7 @@ export default class RegisterBody extends BaseAuthBody {
color="blue"
type="text"
required
placeholder={messages.yourNickname}
placeholder={placeholders.yourNickname}
/>
<Input
@ -35,7 +40,7 @@ export default class RegisterBody extends BaseAuthBody {
color="blue"
type="email"
required
placeholder={messages.yourEmail}
placeholder={placeholders.yourEmail}
/>
<Input
@ -44,7 +49,7 @@ export default class RegisterBody extends BaseAuthBody {
color="blue"
type="password"
required
placeholder={passwordMessages.accountPassword}
placeholder={placeholders.accountPassword}
/>
<Input
@ -53,7 +58,7 @@ export default class RegisterBody extends BaseAuthBody {
color="blue"
type="password"
required
placeholder={messages.repeatPassword}
placeholder={placeholders.repeatPassword}
/>
<Captcha {...this.bindField('captcha')} delay={600} />
@ -65,11 +70,12 @@ export default class RegisterBody extends BaseAuthBody {
required
label={
<Message
{...messages.acceptRules}
key="acceptRules"
defaultMessage="I agree with {link}"
values={{
link: (
<Link to="/rules" target="_blank">
<Message {...messages.termsOfService} />
<Message key="termsOfService" defaultMessage="terms of service" />
</Link>
),
}}

View File

@ -1,5 +0,0 @@
{
"title": "Did not received an Email",
"specifyYourEmail": "Please, enter an Email you've registered with and we will send you new activation code",
"sendNewEmail": "Send new Email"
}

View File

@ -1,8 +1,13 @@
import { defineMessages } from 'react-intl';
import factory from '../factory';
import forgotPasswordMessages from '../forgotPassword/ForgotPassword.intl.json';
import messages from './ResendActivation.intl.json';
import Body from './ResendActivationBody';
const messages = defineMessages({
title: 'Did not received an Email',
sendNewEmail: 'Send new Email',
alreadyHaveCode: 'Already have a code',
});
export default factory({
title: messages.title,
body: Body,
@ -11,6 +16,6 @@ export default factory({
label: messages.sendNewEmail,
},
links: {
label: forgotPasswordMessages.alreadyHaveCode,
label: messages.alreadyHaveCode,
},
});

View File

@ -1,11 +1,13 @@
import React from 'react';
import { FormattedMessage as Message } from 'react-intl';
import { defineMessages, FormattedMessage as Message } from 'react-intl';
import { Input, Captcha } from 'app/components/ui/form';
import BaseAuthBody from '../BaseAuthBody';
import registerMessages from '../register/Register.intl.json';
import styles from './resendActivation.scss';
import messages from './ResendActivation.intl.json';
const placeholders = defineMessages({
yourEmail: 'Your Email',
});
export default class ResendActivation extends BaseAuthBody {
static displayName = 'ResendActivation';
@ -20,7 +22,10 @@ export default class ResendActivation extends BaseAuthBody {
{this.renderErrors()}
<div className={styles.description}>
<Message {...messages.specifyYourEmail} />
<Message
key="specifyYourEmail"
defaultMessage="Please, enter an Email you've registered with and we will send you new activation code"
/>
</div>
<Input
@ -29,7 +34,7 @@ export default class ResendActivation extends BaseAuthBody {
color="blue"
type="email"
required
placeholder={registerMessages.yourEmail}
placeholder={placeholders.yourEmail}
defaultValue={this.context.user.email}
/>

View File

@ -1,7 +1,7 @@
import React from 'react';
import { connect } from 'react-redux';
import clsx from 'clsx';
import { FormattedMessage as Message } from 'react-intl';
import { FormattedMessage as Message, defineMessages } from 'react-intl';
import { Input, TextArea, Button, Form, FormModel, Dropdown } from 'app/components/ui/form';
import feedback from 'app/services/api/feedback';
import icons from 'app/components/ui/icons.scss';
@ -11,17 +11,27 @@ import logger from 'app/services/logger';
import { User } from 'app/components/user';
import styles from './contactForm.scss';
import messages from './contactForm.intl.json';
const CONTACT_CATEGORIES = {
// TODO: сюда позже проставить реальные id категорий с backend
0: <Message {...messages.cannotAccessMyAccount} />,
1: <Message {...messages.foundBugOnSite} />,
2: <Message {...messages.improvementsSuggestion} />,
3: <Message {...messages.integrationQuestion} />,
4: <Message {...messages.other} />,
0: <Message key="cannotAccessMyAccount" defaultMessage="Can not access my account" />,
1: <Message key="foundBugOnSite" defaultMessage="I found a bug on the site" />,
2: <Message key="improvementsSuggestion" defaultMessage="I have a suggestion for improving the functional" />,
3: <Message key="integrationQuestion" defaultMessage="Service integration question" />,
4: <Message key="other" defaultMessage="Other" />,
};
const labels = defineMessages({
subject: 'Subject',
email: 'Email',
message: 'Message',
whichQuestion: 'What are you interested in?',
send: 'Send',
close: 'Close',
});
export class ContactForm extends React.Component<
{
onClose: () => void;
@ -54,7 +64,7 @@ export class ContactForm extends React.Component<
<div className={popupStyles.popup}>
<div className={popupStyles.header}>
<h2 className={popupStyles.headerTitle}>
<Message {...messages.title} />
<Message key="title" defaultMessage="Feedback form" />
</h2>
<span
className={clsx(icons.close, popupStyles.close)}
@ -78,24 +88,30 @@ export class ContactForm extends React.Component<
<Form form={form} onSubmit={this.onSubmit} isLoading={isLoading}>
<div className={popupStyles.body}>
<div className={styles.philosophicalThought}>
<Message {...messages.philosophicalThought} />
<Message
key="philosophicalThought"
defaultMessage="Properly formulated question — half of the answer"
/>
</div>
<div className={styles.formDisclaimer}>
<Message {...messages.disclaimer} />
<Message
key="disclaimer"
defaultMessage="Please formulate your feedback providing as much useful information, as possible to help us understand your problem and solve it"
/>
<br />
</div>
<div className={styles.pairInputRow}>
<div className={styles.pairInput}>
<Input {...form.bindField('subject')} required label={messages.subject} skin="light" />
<Input {...form.bindField('subject')} required label={labels.subject} skin="light" />
</div>
<div className={styles.pairInput}>
<Input
{...form.bindField('email')}
required
label={messages.email}
label={labels.email}
type="email"
skin="light"
defaultValue={user.email}
@ -106,7 +122,7 @@ export class ContactForm extends React.Component<
<div className={styles.formMargin}>
<Dropdown
{...form.bindField('category')}
label={messages.whichQuestion}
label={labels.whichQuestion}
items={CONTACT_CATEGORIES}
block
/>
@ -115,7 +131,7 @@ export class ContactForm extends React.Component<
<TextArea
{...form.bindField('message')}
required
label={messages.message}
label={labels.message}
skin="light"
minRows={6}
maxRows={6}
@ -123,7 +139,7 @@ export class ContactForm extends React.Component<
</div>
<div className={styles.footer}>
<Button label={messages.send} block type="submit" disabled={isLoading} />
<Button label={labels.send} block type="submit" disabled={isLoading} />
</div>
</Form>
);
@ -138,13 +154,16 @@ export class ContactForm extends React.Component<
<div className={styles.successBody}>
<span className={styles.successIcon} />
<div className={styles.successDescription}>
<Message {...messages.youMessageReceived} />
<Message
key="youMessageReceived"
defaultMessage="Your message was received. We will respond to you shortly. The answer will come to your Email:"
/>
</div>
<div className={styles.sentToEmail}>{email}</div>
</div>
<div className={styles.footer}>
<Button label={messages.close} block onClick={onClose} data-testid="feedback-popup-close-button" />
<Button label={labels.close} block onClick={onClose} data-testid="feedback-popup-close-button" />
</div>
</div>
);

View File

@ -1,19 +0,0 @@
{
"title": "Feedback form",
"subject": "Subject",
"email": "Email",
"message": "Message",
"send": "Send",
"philosophicalThought": "Properly formulated question — half of the answer",
"disclaimer": "Please formulate your feedback providing as much useful information, as possible to help us understand your problem and solve it",
"whichQuestion": "What are you interested in?",
"cannotAccessMyAccount": "Can not access my account",
"foundBugOnSite": "I found a bug on the site",
"improvementsSuggestion": "I have a suggestion for improving the functional",
"integrationQuestion": "Service integration question",
"other": "Other",
"youMessageReceived": "Your message was received. We will respond to you shortly. The answer will come to your Email:",
"close": "Close"
}

View File

@ -1,26 +0,0 @@
{
"accountsForDevelopers": "Ely.by Accounts for developers",
"accountsAllowsYouYoUseOauth2": "Ely.by Accounts service provides users with a quick and easy-to-use way to login to your site, launcher or Minecraft server via OAuth2 authorization protocol. You can find more information about integration with Ely.by Accounts in {ourDocumentation}.",
"ourDocumentation": "our documentation",
"ifYouHaveAnyTroubles": "If you are experiencing difficulties, you can always use {feedback}. We'll surely help you.",
"feedback": "feedback",
"weDontKnowAnythingAboutYou": "We don't know anything about you yet.",
"youMustAuthToBegin": "You have to authorize to start.",
"authorization": "Authorization",
"youDontHaveAnyApplication": "You don't have any app registered yet.",
"shallWeStart": "Shall we start?",
"addNew": "Add new",
"yourApplications": "Your applications:",
"countUsers": "{count, plural, =0 {No users} one {# user} other {# users}}",
"ifYouSuspectingThatSecretHasBeenCompromised": "If you are suspecting that your Client Secret has been compromised, then you may want to reset it value. It'll cause recall of the all \"access\" and \"refresh\" tokens that have been issued. You can also recall all issued tokens without changing Client Secret.",
"revokeAllTokens": "Revoke all tokens",
"resetClientSecret": "Reset Client Secret",
"delete": "Delete",
"editDescription": "{icon} Edit description",
"allRefreshTokensWillBecomeInvalid": "All \"refresh\" tokens will become invalid and after next authorization the user will get permissions prompt.",
"appAndAllTokenWillBeDeleted": "Application and all associated tokens will be deleted.",
"takeCareAccessTokensInvalidation": "Take care because \"access\" tokens won't be invalidated immediately.",
"cancel": "Cancel",
"continue": "Continue",
"performing": "Performing…"
}

View File

@ -1,6 +1,6 @@
import React from 'react';
import clsx from 'clsx';
import { FormattedMessage as Message } from 'react-intl';
import { defineMessages, FormattedMessage as Message } from 'react-intl';
import { Helmet } from 'react-helmet-async';
import { LinkButton } from 'app/components/ui/form';
import { COLOR_GREEN, COLOR_BLUE } from 'app/components/ui';
@ -8,12 +8,16 @@ import { ContactLink } from 'app/components/contact';
import { OauthAppResponse } from 'app/services/api/oauth';
import styles from './applicationsIndex.scss';
import messages from './ApplicationsIndex.intl.json';
import cubeIcon from './icons/cube.svg';
import loadingCubeIcon from './icons/loading-cube.svg';
import toolsIcon from './icons/tools.svg';
import ApplicationsList from './list';
const labels = defineMessages({
addNew: 'Add new',
authorization: 'Authorization',
});
type Props = {
clientId: string | null;
resetClientId: () => void; // notify parent to remove clientId from current location.href
@ -29,7 +33,7 @@ export default class ApplicationsIndex extends React.Component<Props> {
return (
<div className={styles.container}>
<div className={styles.welcomeContainer}>
<Message {...messages.accountsForDevelopers}>
<Message key="accountsForDevelopers" defaultMessage="Ely.by Accounts for developers">
{(pageTitle: string) => (
<h2 className={styles.welcomeTitle}>
<Helmet title={pageTitle} />
@ -40,11 +44,12 @@ export default class ApplicationsIndex extends React.Component<Props> {
<div className={styles.welcomeTitleDelimiter} />
<div className={styles.welcomeParagraph}>
<Message
{...messages.accountsAllowsYouYoUseOauth2}
key="accountsAllowsYouYoUseOauth2"
defaultMessage="Ely.by Accounts service provides users with a quick and easy-to-use way to login to your site, launcher or Minecraft server via OAuth2 authorization protocol. You can find more information about integration with Ely.by Accounts in {ourDocumentation}."
values={{
ourDocumentation: (
<a href="https://docs.ely.by/en/oauth.html" target="_blank">
<Message {...messages.ourDocumentation} />
<Message key="ourDocumentation" defaultMessage="our documentation" />
</a>
),
}}
@ -52,11 +57,12 @@ export default class ApplicationsIndex extends React.Component<Props> {
</div>
<div className={styles.welcomeParagraph}>
<Message
{...messages.ifYouHaveAnyTroubles}
key="ifYouHaveAnyTroubles"
defaultMessage="If you are experiencing difficulties, you can always use {feedback}. We'll surely help you."
values={{
feedback: (
<ContactLink>
<Message {...messages.feedback} />
<Message key="feedback" defaultMessage="feedback" />
</ContactLink>
),
}}
@ -104,17 +110,20 @@ function Loader({ noApps }: { noApps: boolean }) {
>
<div className={styles.emptyStateText}>
<div>
<Message {...messages.youDontHaveAnyApplication} />
<Message
key="youDontHaveAnyApplication"
defaultMessage="You don't have any app registered yet."
/>
</div>
<div>
<Message {...messages.shallWeStart} />
<Message key="shallWeStart" defaultMessage="Shall we start?" />
</div>
</div>
<LinkButton
to="/dev/applications/new"
data-e2e="newApp"
label={messages.addNew}
label={labels.addNew}
color={COLOR_GREEN}
className={styles.emptyStateActionButton}
/>
@ -129,16 +138,16 @@ function Guest() {
<img src={toolsIcon} className={styles.emptyStateIcon} />
<div className={styles.emptyStateText}>
<div>
<Message {...messages.weDontKnowAnythingAboutYou} />
<Message key="weDontKnowAnythingAboutYou" defaultMessage="We don't know anything about you yet." />
</div>
<div>
<Message {...messages.youMustAuthToBegin} />
<Message key="youMustAuthToBegin" defaultMessage="You have to authorize to start." />
</div>
</div>
<LinkButton
to="/login"
label={messages.authorization}
label={labels.authorization}
color={COLOR_BLUE}
className={styles.emptyStateActionButton}
/>

View File

@ -1,20 +0,0 @@
{
"creatingApplication": "Creating an application",
"website": "Web site",
"minecraftServer": "Minecraft server",
"toDisplayRegistrationFormChooseType": "To display registration form for a new application choose necessary type.",
"applicationName": "Application name:",
"appDescriptionWillBeAlsoVisibleOnOauthPage": "Application's description will be displayed at the authorization page too. It isn't a required field. In authorization process the value may be overridden.",
"description": "Description:",
"websiteLinkWillBeUsedAsAdditionalId": "Site's link is optional, but it can be used as an additional identifier of the application.",
"websiteLink": "Website link:",
"redirectUriLimitsAllowableBaseAddress": "Redirection URI (redirectUri) determines a base address, that user will be allowed to be redirected to after authorization. In order to improve security it's better to use the whole path instead of just a domain name. For example: https://example.com/oauth/ely.",
"redirectUri": "Redirect URI:",
"createApplication": "Create application",
"serverName": "Server name:",
"ipAddressIsOptionButPreferable": "IP address is optional, but is very preferable. It might become handy in case of we suddenly decide to play on your server with the entire band (=",
"serverIp": "Server IP:",
"youCanAlsoSpecifyServerSite": "You also can specify either server's site URL or its community in a social network.",
"updatingApplication": "Updating an application",
"updateApplication": "Update application"
}

View File

@ -1,5 +1,5 @@
import React from 'react';
import { FormattedMessage as Message } from 'react-intl';
import { FormattedMessage as Message, defineMessages } from 'react-intl';
import { Helmet } from 'react-helmet-async';
import { MessageDescriptor } from 'react-intl';
import { OauthAppResponse } from 'app/services/api/oauth';
@ -10,12 +10,22 @@ import { COLOR_GREEN } from 'app/components/ui';
import { TYPE_APPLICATION, TYPE_MINECRAFT_SERVER } from 'app/components/dev/apps';
import styles from 'app/components/profile/profileForm.scss';
import logger from 'app/services/logger';
import messages from './ApplicationForm.intl.json';
import ApplicationTypeSwitcher from './ApplicationTypeSwitcher';
import WebsiteType from './WebsiteType';
import MinecraftServerType from './MinecraftServerType';
const messages = defineMessages({
website: 'Web site',
minecraftServer: 'Minecraft server',
creatingApplication: 'Creating an application',
createApplication: 'Create application',
updatingApplication: 'Updating an application',
updateApplication: 'Update application',
});
type TypeToForm = Record<
ApplicationType,
{
@ -94,7 +104,10 @@ export default class ApplicationForm extends React.Component<{
) : (
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.toDisplayRegistrationFormChooseType} />
<Message
key="toDisplayRegistrationFormChooseType"
defaultMessage="To display registration form for a new application choose necessary type."
/>
</p>
</div>
)}

View File

@ -1,11 +1,18 @@
import React, { ComponentType } from 'react';
import { FormattedMessage as Message } from 'react-intl';
import { FormattedMessage as Message, defineMessages } from 'react-intl';
import { OauthAppResponse } from 'app/services/api/oauth';
import { Input, FormModel } from 'app/components/ui/form';
import { SKIN_LIGHT } from 'app/components/ui';
import styles from 'app/components/profile/profileForm.scss';
import messages from './ApplicationForm.intl.json';
const messages = defineMessages({
serverName: 'Server name:',
ipAddressIsOptionButPreferable:
'IP address is optional, but is very preferable. It might become handy in case of we suddenly decide to play on your server with the entire band (=',
serverIp: 'Server IP:',
youCanAlsoSpecifyServerSite: "You also can specify either server's site URL or its community in a social network.",
websiteLink: 'Website link:',
});
interface Props {
form: FormModel;

View File

@ -1,11 +1,22 @@
import React, { ComponentType } from 'react';
import { FormattedMessage as Message } from 'react-intl';
import { defineMessages, FormattedMessage as Message } from 'react-intl';
import { Input, TextArea, FormModel } from 'app/components/ui/form';
import { OauthAppResponse } from 'app/services/api/oauth';
import { SKIN_LIGHT } from 'app/components/ui';
import styles from 'app/components/profile/profileForm.scss';
import messages from './ApplicationForm.intl.json';
const messages = defineMessages({
applicationName: 'Application name:',
appDescriptionWillBeAlsoVisibleOnOauthPage:
"Application's description will be displayed at the authorization page too. It isn't a required field. In authorization process the value may be overridden.",
description: 'Description:',
websiteLinkWillBeUsedAsAdditionalId:
"Site's link is optional, but it can be used as an additional identifier of the application.",
websiteLink: 'Website link:',
redirectUriLimitsAllowableBaseAddress:
"Redirection URI (redirectUri) determines a base address, that user will be allowed to be redirected to after authorization. In order to improve security it's better to use the whole path instead of just a domain name. For example: https://example.com/oauth/ely.",
redirectUri: 'Redirect URI:',
});
interface Props {
form: FormModel;

View File

@ -0,0 +1,24 @@
/**
* Extract this file to the level upper to keep the messages prefix. Will be resolved later.
*/
import { defineMessages } from 'react-intl';
export default defineMessages({
revokeAllTokens: 'Revoke all tokens',
resetClientSecret: 'Reset Client Secret',
delete: 'Delete',
countUsers: '{count, plural, =0 {No users} one {# user} other {# users}}',
editDescription: '{icon} Edit description',
ifYouSuspectingThatSecretHasBeenCompromised:
'If you are suspecting that your Client Secret has been compromised, then you may want to reset it value. It\'ll cause recall of the all "access" and "refresh" tokens that have been issued. You can also recall all issued tokens without changing Client Secret.',
allRefreshTokensWillBecomeInvalid:
'All "refresh" tokens will become invalid and after next authorization the user will get permissions prompt.',
takeCareAccessTokensInvalidation: 'Take care because "access" tokens won\'t be invalidated immediately.',
cancel: 'Cancel',
performing: 'Performing…',
continue: 'Continue',
appAndAllTokenWillBeDeleted: 'Application and all associated tokens will be deleted.',
yourApplications: 'Your applications:',
addNew: 'Add new',
});

View File

@ -8,7 +8,7 @@ import { OauthAppResponse } from 'app/services/api/oauth';
import Collapse from 'app/components/ui/collapse';
import styles from '../applicationsIndex.scss';
import messages from '../ApplicationsIndex.intl.json';
import messages from '../list.intl';
const ACTION_REVOKE_TOKENS = 'revoke-tokens';
const ACTION_RESET_SECRET = 'reset-secret';

View File

@ -5,7 +5,7 @@ import { LinkButton } from 'app/components/ui/form';
import { COLOR_GREEN } from 'app/components/ui';
import { OauthAppResponse } from 'app/services/api/oauth';
import messages from '../ApplicationsIndex.intl.json';
import messages from '../list.intl';
import styles from '../applicationsIndex.scss';
import ApplicationItem from './ApplicationItem';

View File

@ -7,7 +7,6 @@ import { create as createPopup } from 'app/components/ui/popup/actions';
import { ContactLink } from 'app/components/contact';
import styles from './footerMenu.scss';
import messages from './footerMenu.intl.json';
const FooterMenu: ComponentType = () => {
const dispatch = useDispatch();
@ -22,19 +21,19 @@ const FooterMenu: ComponentType = () => {
return (
<div className={styles.footerMenu} data-testid="footer">
<Link to="/rules" className={styles.footerItem}>
<Message {...messages.rules} />
<Message key="rules" defaultMessage="Rules" />
</Link>
<ContactLink className={styles.footerItem}>
<Message {...messages.contactUs} />
<Message key="contactUs" defaultMessage="Contact Us" />
</ContactLink>
<Link to="/dev" className={styles.footerItem}>
<Message {...messages.forDevelopers} />
<Message key="forDevelopers" defaultMessage="For developers" />
</Link>
<div className={styles.langTriggerContainer}>
<a href="#" className={styles.langTrigger} onClick={onLanguageSwitcherClick}>
<span className={styles.langTriggerIcon} />
<Message {...messages.siteLanguage} />
<Message key="siteLanguage" defaultMessage="Site language" />
</a>
</div>
</div>

View File

@ -1,6 +0,0 @@
{
"rules": "Rules",
"contactUs": "Contact Us",
"siteLanguage": "Site language",
"forDevelopers": "For developers"
}

View File

@ -5,12 +5,14 @@ import i18n from 'app/services/i18n';
import { RootState } from 'app/reducers';
const IntlProvider: ComponentType = ({ children }) => {
const [intl, setIntl] = useState<IntlShape>(i18n.getIntl());
const [intl, setIntl] = useState<IntlShape>();
const locale = useSelector(({ i18n: i18nState }: RootState) => i18nState.locale);
useEffect(() => {
if (process.env.NODE_ENV === 'test') {
// disable async modules loading in tests
setIntl(i18n.getIntl());
return;
}
@ -19,6 +21,11 @@ const IntlProvider: ComponentType = ({ children }) => {
})();
}, [locale]);
// don't run the application until locale bundle will be loaded
if (!intl) {
return null;
}
return <RawIntlProvider value={intl}>{children}</RawIntlProvider>;
};

View File

@ -12,7 +12,6 @@ import { FormattedMessage as Message } from 'react-intl';
import clsx from 'clsx';
import LocaleItem from './LocaleItem';
import messages from './languageSwitcher.intl.json';
import { LocalesMap } from './LanguageSwitcher';
import styles from './languageSwitcher.scss';
@ -92,7 +91,10 @@ export default class LanguageList extends React.Component<{
className={styles.emptyLanguagesListCaption}
/>
<div className={styles.emptyLanguagesListSubtitle}>
<Message {...messages.weDoNotSupportThisLang} />
<Message
key="weDoNotSupportThisLang"
defaultMessage="Sorry, we do not support this language"
/>
</div>
</div>
</div>

View File

@ -9,7 +9,6 @@ import popupStyles from 'app/components/ui/popup/popup.scss';
import icons from 'app/components/ui/icons.scss';
import styles from './languageSwitcher.scss';
import messages from './languageSwitcher.intl.json';
import LanguageList from './LanguageList';
import { RootState } from 'app/reducers';
@ -70,7 +69,7 @@ class LanguageSwitcher extends React.Component<
<div className={popupStyles.popup}>
<div className={popupStyles.header}>
<h2 className={popupStyles.headerTitle}>
<Message {...messages.siteLanguage} />
<Message key="siteLanguage" defaultMessage="Site language" />
</h2>
<span className={clsx(icons.close, popupStyles.close)} onClick={onClose} />
</div>
@ -79,7 +78,10 @@ class LanguageSwitcher extends React.Component<
<div className={styles.searchBox}>
<input
className={clsx(formStyles.lightTextField, formStyles.greenTextField)}
placeholder={intl.formatMessage(messages.startTyping)}
placeholder={intl.formatMessage({
key: 'startTyping',
defaultMessage: 'Start typing…',
})}
onChange={this.onFilterUpdate}
onKeyPress={this.onFilterKeyPress()}
autoFocus
@ -97,12 +99,18 @@ class LanguageSwitcher extends React.Component<
<div className={styles.improveTranslatesIcon} />
<div className={styles.improveTranslatesContent}>
<div className={styles.improveTranslatesTitle}>
<Message {...messages.improveTranslates} />
<Message key="improveTranslates" defaultMessage="Improve Ely.by translation" />
</div>
<div className={styles.improveTranslatesText}>
<Message {...messages.improveTranslatesDescription} />{' '}
<Message
key="improveTranslatesDescription"
defaultMessage="Ely.bys localization is a community effort. If you want to improve the translation of Ely.by, we'd love your help."
/>{' '}
<a href={translateUrl} target="_blank">
<Message {...messages.improveTranslatesParticipate} />
<Message
key="improveTranslatesParticipate"
defaultMessage="Click here to participate."
/>
</a>
</div>
</div>

View File

@ -2,7 +2,6 @@ import React, { ComponentType, ReactNode } from 'react';
import { localeFlags } from 'app/components/i18n';
import { FormattedMessage as Message } from 'react-intl';
import messages from './languageSwitcher.intl.json';
import styles from './languageSwitcher.scss';
import { LocaleData } from './LanguageSwitcher';
@ -16,14 +15,15 @@ const LocaleItem: ComponentType<Props> = ({ locale: { code, name, englishName, p
if (progress !== 100) {
progressLabel = (
<Message
{...messages.translationProgress}
key="translationProgress"
defaultMessage="{progress}% translated"
values={{
progress,
}}
/>
);
} else if (!isReleased) {
progressLabel = <Message {...messages.mayBeInaccurate} />;
progressLabel = <Message key="mayBeInaccurate" defaultMessage="May be inaccurate" />;
}
return (

View File

@ -1,10 +0,0 @@
{
"siteLanguage": "Site language",
"startTyping": "Start typing…",
"translationProgress": "{progress}% translated",
"mayBeInaccurate": "May be inaccurate",
"weDoNotSupportThisLang": "Sorry, we do not support this language",
"improveTranslates": "Improve Ely.by translation",
"improveTranslatesDescription": "Ely.bys localization is a community effort. If you want to improve the translation of Ely.by, we'd love your help.",
"improveTranslatesParticipate": "Click here to participate."
}

View File

@ -1,19 +0,0 @@
{
"accountPreferencesTitle": "Ely.by account preferences",
"personalData": "Personal data",
"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.",
"preferencesDescription": "Here you can change the key preferences of your account. Please note that all actions must be confirmed by entering a password.",
"mojangPriorityWarning": "A Mojang account with the same nickname was found. According to {rules}, account owner has the right to demand the restoration of control over nickname.",
"projectRules": "project rules",
"changedAt": "Changed {at}",
"disabled": "Disabled",
"enabled": "Enabled",
"nickname": "Nickname:",
"email": "Email:",
"password": "Password:",
"siteLanguage": "Site language:",
"languageIsUnavailableWarning": "The locale \"{locale}\" you've used earlier isn't currently translated enough. If you want to continue using the selected language, please {participateInTheTranslation} of the project.",
"participateInTheTranslation": "participate in the translation",
"twoFactorAuth": "Twofactor auth:",
"uuid": "UUID:"
}

View File

@ -12,7 +12,6 @@ import { RootState } from 'app/reducers';
import ProfileField from './ProfileField';
import styles from './profile.scss';
import profileForm from './profileForm.scss';
import messages from './Profile.intl.json';
type Props = {
user: User;
@ -27,7 +26,7 @@ class Profile extends React.PureComponent<Props> {
return (
<div data-testid="profile-index">
<Message {...messages.accountPreferencesTitle}>
<Message key="accountPreferencesTitle" defaultMessage="Ely.by account preferences">
{(pageTitle: string) => (
<h2 className={styles.indexTitle}>
<Helmet title={pageTitle} />
@ -39,7 +38,10 @@ class Profile extends React.PureComponent<Props> {
<div className={styles.indexContent}>
<div className={styles.descriptionColumn}>
<div className={styles.indexDescription}>
<Message {...messages.accountDescription} />
<Message
key="accountDescription"
defaultMessage="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."
/>
</div>
</div>
@ -47,21 +49,25 @@ class Profile extends React.PureComponent<Props> {
<div className={profileForm.form}>
<div className={styles.item}>
<h3 className={profileForm.title}>
<Message {...messages.personalData} />
<Message key="personalData" defaultMessage="Personal data" />
</h3>
<p className={profileForm.description}>
<Message {...messages.preferencesDescription} />
<Message
key="preferencesDescription"
defaultMessage="Here you can change the key preferences of your account. Please note that all actions must be confirmed by entering a password."
/>
</p>
</div>
<ProfileField
link="/profile/change-username"
label={<Message {...messages.nickname} />}
label={<Message key="nickname" defaultMessage="Nickname:" />}
value={user.username}
warningMessage={
user.hasMojangUsernameCollision ? (
<Message
{...messages.mojangPriorityWarning}
key="mojangPriorityWarning"
defaultMessage="A Mojang account with the same nickname was found. According to {rules}, account owner has the right to demand the restoration of control over nickname."
values={{
rules: (
<Link
@ -70,7 +76,7 @@ class Profile extends React.PureComponent<Props> {
hash: `#${RulesPage.getRuleHash(1, 4)}`,
}}
>
<Message {...messages.projectRules} />
<Message key="projectRules" defaultMessage="project rules" />
</Link>
),
}}
@ -83,16 +89,17 @@ class Profile extends React.PureComponent<Props> {
<ProfileField
link="/profile/change-email"
label={<Message {...messages.email} />}
label={<Message key="email" defaultMessage="Email:" />}
value={user.email}
/>
<ProfileField
link="/profile/change-password"
label={<Message {...messages.password} />}
label={<Message key="password" defaultMessage="Password:" />}
value={
<Message
{...messages.changedAt}
key="changedAt"
defaultMessage="Changed {at}"
values={{
at: <RelativeTime timestamp={user.passwordChangedAt * 1000} />,
}}
@ -101,19 +108,25 @@ class Profile extends React.PureComponent<Props> {
/>
<ProfileField
label={<Message {...messages.siteLanguage} />}
label={<Message key="siteLanguage" defaultMessage="Site language:" />}
value={<ChangeLanguageLink />}
warningMessage={
user.lang === interfaceLocale ? (
''
) : (
<Message
{...messages.languageIsUnavailableWarning}
key="languageIsUnavailableWarning"
defaultMessage={
'The locale "{locale}" you\'ve used earlier isn\'t currently translated enough. If you want to continue using the selected language, please {participateInTheTranslation} of the project.'
}
values={{
locale: user.lang,
participateInTheTranslation: (
<a href="http://ely.by/translate" target="_blank">
<Message {...messages.participateInTheTranslation} />
<Message
key="participateInTheTranslation"
defaultMessage="participate in the translation"
/>
</a>
),
}}
@ -124,18 +137,18 @@ class Profile extends React.PureComponent<Props> {
<ProfileField
link="/profile/mfa"
label={<Message {...messages.twoFactorAuth} />}
label={<Message key="twoFactorAuth" defaultMessage="Twofactor auth:" />}
value={
user.isOtpEnabled ? (
<Message {...messages.enabled} />
<Message key="enabled" defaultMessage="Enabled" />
) : (
<Message {...messages.disabled} />
<Message key="disabled" defaultMessage="Disabled" />
)
}
/>
<ProfileField
label={<Message {...messages.uuid} />}
label={<Message key="uuid" defaultMessage="UUID:" />}
value={
<span
className={styles.uuid}

View File

@ -1,3 +0,0 @@
{
"back": "Back"
}

View File

@ -1,10 +1,13 @@
import React from 'react';
import { FormattedMessage as Message } from 'react-intl';
import { defineMessages, FormattedMessage as Message } from 'react-intl';
import { Link } from 'react-router-dom';
import FormComponent from 'app/components/ui/form/FormComponent';
import styles from './profileForm.scss';
import messages from './ProfileForm.intl.json';
const { back: backMsg } = defineMessages({
back: 'Back',
});
export class BackButton extends FormComponent<{
to: string;
@ -20,12 +23,12 @@ export class BackButton extends FormComponent<{
<Link
className={styles.backButton}
to={to}
title={this.formatMessage(messages.back)}
title={this.formatMessage(backMsg)}
data-testid="back-to-profile"
>
<span className={styles.backIcon} />
<span className={styles.backText}>
<Message {...messages.back} />
<Message {...backMsg} />
</span>
</Link>
);

View File

@ -1,17 +0,0 @@
{
"changeEmailTitle": "Change Email",
"changeEmailDescription": "To change current account Email you must first verify that you own the current address and then confirm the new one.",
"currentAccountEmail": "Current account Email address:",
"pressButtonToStart": "Press the button below to send a message with the code for Email change initialization.",
"enterInitializationCode": "The Email with an initialization code for Email change procedure was sent to {email}. Please enter the code into the field below:",
"enterNewEmail": "Then provide your new Email address, that you want to use with this account. You will be mailed with confirmation code.",
"finalizationCodeWasSentToEmail": "The Email change confirmation code was sent to {email}.",
"enterFinalizationCode": "In order to confirm your new Email, please enter the code received into the field below:",
"newEmailPlaceholder": "Enter new Email",
"codePlaceholder": "Paste the code here",
"sendEmailButton": "Send Email",
"changeEmailButton": "Change Email",
"alreadyReceivedCode": "Already received code"
}

View File

@ -1,5 +1,5 @@
import React, { ReactNode } from 'react';
import { FormattedMessage as Message } from 'react-intl';
import { defineMessages, FormattedMessage as Message } from 'react-intl';
import { Helmet } from 'react-helmet-async';
import { SlideMotion } from 'app/components/ui/motion';
import { ScrollIntoView } from 'app/components/ui/scroll';
@ -10,7 +10,6 @@ import helpLinks from 'app/components/auth/helpLinks.scss';
import Stepper from 'app/components/ui/stepper';
import changeEmail from './changeEmail.scss';
import messages from './ChangeEmail.intl.json';
const STEPS_TOTAL = 3;
@ -40,6 +39,13 @@ interface FormStepParams {
code?: string;
}
const labels = defineMessages({
changeEmailButton: 'Change Email',
sendEmailButton: 'Send Email',
codePlaceholder: 'Paste the code here',
newEmailPlaceholder: 'Enter new Email',
});
export default class ChangeEmail extends React.Component<Props, State> {
static get defaultProps(): Partial<Props> {
return {
@ -73,7 +79,7 @@ export default class ChangeEmail extends React.Component<Props, State> {
<div className={styles.form}>
<div className={styles.formBody}>
<Message {...messages.changeEmailTitle}>
<Message key="changeEmailTitle" defaultMessage="Change Email">
{(pageTitle) => (
<h3 className={styles.violetTitle}>
<Helmet title={pageTitle as string} />
@ -84,7 +90,10 @@ export default class ChangeEmail extends React.Component<Props, State> {
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.changeEmailDescription} />
<Message
key="changeEmailDescription"
defaultMessage="To change current account Email you must first verify that you own the current address and then confirm the new one."
/>
</p>
</div>
</div>
@ -103,14 +112,14 @@ export default class ChangeEmail extends React.Component<Props, State> {
color="violet"
type="submit"
block
label={this.isLastStep() ? messages.changeEmailButton : messages.sendEmailButton}
label={this.isLastStep() ? labels.changeEmailButton : labels.sendEmailButton}
/>
</div>
<div className={helpLinks.helpLinks}>
{this.isLastStep() ? null : (
<a href="#" onClick={this.onSwitchStep}>
<Message {...messages.alreadyReceivedCode} />
<Message key="alreadyReceivedCode" defaultMessage="Already received code" />
</a>
)}
</div>
@ -153,7 +162,7 @@ export default class ChangeEmail extends React.Component<Props, State> {
<div key="step0" data-testid="step1" className={styles.formBody}>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.currentAccountEmail} />
<Message key="currentAccountEmail" defaultMessage="Current account Email address:" />
</p>
</div>
@ -165,7 +174,10 @@ export default class ChangeEmail extends React.Component<Props, State> {
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.pressButtonToStart} />
<Message
key="pressButtonToStart"
defaultMessage="Press the button below to send a message with the code for Email change initialization."
/>
</p>
</div>
</div>
@ -178,7 +190,8 @@ export default class ChangeEmail extends React.Component<Props, State> {
<div className={styles.formRow}>
<p className={styles.description}>
<Message
{...messages.enterInitializationCode}
key="enterInitializationCode"
defaultMessage="The Email with an initialization code for Email change procedure was sent to {email}. Please enter the code into the field below:"
values={{
email: <b>{email}</b>,
}}
@ -196,13 +209,16 @@ export default class ChangeEmail extends React.Component<Props, State> {
autoComplete="off"
skin="light"
color="violet"
placeholder={messages.codePlaceholder}
placeholder={labels.codePlaceholder}
/>
</div>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.enterNewEmail} />
<Message
key="enterNewEmail"
defaultMessage="Then provide your new Email address, that you want to use with this account. You will be mailed with confirmation code."
/>
</p>
</div>
@ -212,7 +228,7 @@ export default class ChangeEmail extends React.Component<Props, State> {
required={isActiveStep}
skin="light"
color="violet"
placeholder={messages.newEmailPlaceholder}
placeholder={labels.newEmailPlaceholder}
/>
</div>
</div>
@ -229,14 +245,18 @@ export default class ChangeEmail extends React.Component<Props, State> {
{newEmail ? (
<span>
<Message
{...messages.finalizationCodeWasSentToEmail}
key="finalizationCodeWasSentToEmail"
defaultMessage="The Email change confirmation code was sent to {email}."
values={{
email: <b>{newEmail}</b>,
}}
/>{' '}
</span>
) : null}
<Message {...messages.enterFinalizationCode} />
<Message
key="enterFinalizationCode"
defaultMessage="In order to confirm your new Email, please enter the code received into the field below:"
/>
</p>
</div>
@ -250,7 +270,7 @@ export default class ChangeEmail extends React.Component<Props, State> {
autoComplete="off"
skin="light"
color="violet"
placeholder={messages.codePlaceholder}
placeholder={labels.codePlaceholder}
/>
</div>
</div>

View File

@ -1,10 +0,0 @@
{
"changePasswordTitle": "Change password",
"changePasswordDescription": "Please take a password, that will be different from your passwords on the other sites and will not be the same you are using to enter Minecraft game servers you are playing.",
"achievementLossWarning": "Are you cherish your game achievements, right?",
"passwordRequirements": "Password must contain at least 8 characters. It can be any symbols — do not limit yourself, create an unpredictable password!",
"changePasswordButton": "Change password",
"newPasswordLabel": "New password:",
"repeatNewPasswordLabel": "Repeat the password:",
"logoutOnAllDevices": "Logout on all devices"
}

View File

@ -1,11 +1,17 @@
import React from 'react';
import { FormattedMessage as Message } from 'react-intl';
import { defineMessages, FormattedMessage as Message } from 'react-intl';
import { Helmet } from 'react-helmet-async';
import { Input, Button, Checkbox, Form, FormModel } from 'app/components/ui/form';
import { BackButton } from '../ProfileForm';
import styles from '../profileForm.scss';
import messages from './ChangePassword.intl.json';
const labels = defineMessages({
changePasswordButton: 'Change password',
newPasswordLabel: 'New password:',
repeatNewPasswordLabel: 'Repeat the password:',
logoutOnAllDevices: 'Logout on all devices',
});
interface Props {
form: FormModel;
@ -29,7 +35,7 @@ export default class ChangePassword extends React.Component<Props> {
<div className={styles.form}>
<div className={styles.formBody}>
<Message {...messages.changePasswordTitle}>
<Message key="changePasswordTitle" defaultMessage="Change password">
{(pageTitle) => (
<h3 className={styles.title}>
<Helmet title={pageTitle as string} />
@ -40,10 +46,16 @@ export default class ChangePassword extends React.Component<Props> {
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.changePasswordDescription} />
<Message
key="changePasswordDescription"
defaultMessage="Please take a password, that will be different from your passwords on the other sites and will not be the same you are using to enter Minecraft game servers you are playing."
/>
<br />
<b>
<Message {...messages.achievementLossWarning} />
<Message
key="achievementLossWarning"
defaultMessage="Are you cherish your game achievements, right?"
/>
</b>
</p>
</div>
@ -54,13 +66,16 @@ export default class ChangePassword extends React.Component<Props> {
type="password"
required
skin="light"
label={messages.newPasswordLabel}
label={labels.newPasswordLabel}
/>
</div>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.passwordRequirements} />
<Message
key="passwordRequirements"
defaultMessage="Password must contain at least 8 characters. It can be any symbols — do not limit yourself, create an unpredictable password!"
/>
</p>
</div>
@ -70,7 +85,7 @@ export default class ChangePassword extends React.Component<Props> {
type="password"
required
skin="light"
label={messages.repeatNewPasswordLabel}
label={labels.repeatNewPasswordLabel}
/>
</div>
@ -79,12 +94,12 @@ export default class ChangePassword extends React.Component<Props> {
{...form.bindField('logoutAll')}
defaultChecked
skin="light"
label={messages.logoutOnAllDevices}
label={labels.logoutOnAllDevices}
/>
</div>
</div>
<Button color="green" block label={messages.changePasswordButton} type="submit" />
<Button color="green" block label={labels.changePasswordButton} type="submit" />
</div>
</div>
</Form>

View File

@ -1,6 +0,0 @@
{
"changeUsernameTitle": "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.",
"changeUsernameWarning": "Be careful: if you playing on the server with nickname binding, then after changing nickname you may lose all your progress.",
"changeUsernameButton": "Change nickname"
}

View File

@ -1,11 +1,14 @@
import React from 'react';
import { FormattedMessage as Message } from 'react-intl';
import { defineMessages, FormattedMessage as Message } from 'react-intl';
import { Helmet } from 'react-helmet-async';
import { Input, Button, Form, FormModel } from 'app/components/ui/form';
import { BackButton } from 'app/components/profile/ProfileForm';
import styles from '../profileForm.scss';
import messages from './ChangeUsername.intl.json';
const labels = defineMessages({
changeUsernameButton: 'Change nickname',
});
interface Props {
username: string;
@ -31,7 +34,7 @@ export default class ChangeUsername extends React.Component<Props> {
<div className={styles.form}>
<div className={styles.formBody}>
<Message {...messages.changeUsernameTitle}>
<Message key="changeUsernameTitle" defaultMessage="Change nickname">
{(pageTitle) => (
<h3 className={styles.title}>
<Helmet title={pageTitle as string} />
@ -42,7 +45,10 @@ export default class ChangeUsername extends React.Component<Props> {
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.changeUsernameDescription} />
<Message
key="changeUsernameDescription"
defaultMessage="You can change your nickname to any arbitrary value. Remember that it is not recommended to take a nickname of already existing Mojang account."
/>
</p>
</div>
@ -58,12 +64,15 @@ export default class ChangeUsername extends React.Component<Props> {
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.changeUsernameWarning} />
<Message
key="changeUsernameWarning"
defaultMessage="Be careful: if you playing on the server with nickname binding, then after changing nickname you may lose all your progress."
/>
</p>
</div>
</div>
<Button color="green" block label={messages.changeUsernameButton} type="submit" />
<Button color="green" block label={labels.changeUsernameButton} type="submit" />
</div>
</div>
</Form>

View File

@ -1,10 +1,12 @@
import React from 'react';
import { FormattedMessage as Message } from 'react-intl';
import { defineMessages, FormattedMessage as Message } from 'react-intl';
import { Input, Form, FormModel } from 'app/components/ui/form';
import profileForm from 'app/components/profile/profileForm.scss';
import messages from '../MultiFactorAuth.intl.json';
const messages = defineMessages({
codePlaceholder: 'Enter the code here',
});
export default function Confirmation({
form,
@ -22,7 +24,10 @@ export default function Confirmation({
<div className={profileForm.formBody}>
<div className={profileForm.formRow}>
<p className={profileForm.description}>
<Message {...messages.enterCodeFromApp} />
<Message
key="enterCodeFromApp"
defaultMessage="In order to finish twofactor auth setup, please enter the code received in the mobile app:"
/>
</p>
</div>

View File

@ -4,8 +4,8 @@ import { disable as disableMFA } from 'app/services/api/mfa';
import { FormModel } from 'app/components/ui/form';
import Context from '../Context';
import MfaDisableForm from './disableForm/MfaDisableForm';
import MfaStatus from './status/MfaStatus';
import MfaDisableForm from './MfaDisableForm';
import MfaStatus from './MfaStatus';
export default class MfaDisable extends React.Component<
{

View File

@ -1,10 +1,14 @@
import React from 'react';
import { FormattedMessage as Message } from 'react-intl';
import { defineMessages, FormattedMessage as Message } from 'react-intl';
import { Button, Input, Form, FormModel } from 'app/components/ui/form';
import styles from 'app/components/profile/profileForm.scss';
import messages from '../MultiFactorAuth.intl.json';
import mfaStyles from '../mfa.scss';
import mfaStyles from './mfa.scss';
const messages = defineMessages({
codePlaceholder: 'Enter the code here',
disable: 'Disable',
});
export default class MfaDisableForm extends React.Component<{
onSubmit: (form: FormModel) => Promise<void>;
@ -20,13 +24,16 @@ export default class MfaDisableForm extends React.Component<{
<div className={styles.formBody}>
<div className={styles.formRow}>
<p className={`${styles.description} ${mfaStyles.mfaTitle}`}>
<Message {...messages.disableMfa} />
<Message key="disableMfa" defaultMessage="Disable twofactor authentication" />
</p>
</div>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.disableMfaInstruction} />
<Message
key="disableMfaInstruction"
defaultMessage="In order to disable twofactor authentication, you need to provide a code from your mobile app and confirm your action with your current account password."
/>
</p>
</div>

View File

@ -7,12 +7,12 @@ import { ScrollIntoView } from 'app/components/ui/scroll';
import logger from 'app/services/logger';
import { getSecret, enable as enableMFA } from 'app/services/api/mfa';
import { Form } from 'app/components/ui/form';
import { defineMessages } from 'react-intl';
import Context from '../Context';
import Instructions from './instructions';
import KeyForm from './keyForm';
import Confirmation from './confirmation';
import messages from './MultiFactorAuth.intl.json';
import Confirmation from './Confirmation';
const STEPS_TOTAL = 3;
@ -25,6 +25,12 @@ type Props = {
step: MfaStep;
};
const labels = defineMessages({
theAppIsInstalled: 'App has been installed',
ready: 'Ready',
enable: 'Enable',
});
interface State {
isLoading: boolean;
activeStep: MfaStep;
@ -73,15 +79,15 @@ export default class MfaEnable extends React.PureComponent<Props, State> {
const stepsData = [
{
buttonLabel: messages.theAppIsInstalled,
buttonLabel: labels.theAppIsInstalled,
buttonAction: () => this.nextStep(),
},
{
buttonLabel: messages.ready,
buttonLabel: labels.ready,
buttonAction: () => this.nextStep(),
},
{
buttonLabel: messages.enable,
buttonLabel: labels.enable,
buttonAction: () => this.confirmationFormEl && this.confirmationFormEl.submit(),
},
];

View File

@ -4,8 +4,7 @@ import { ScrollIntoView } from 'app/components/ui/scroll';
import styles from 'app/components/profile/profileForm.scss';
import icons from 'app/components/ui/icons.scss';
import messages from '../MultiFactorAuth.intl.json';
import mfaStyles from '../mfa.scss';
import mfaStyles from './mfa.scss';
export default function MfaStatus({ onProceed }: { onProceed: () => void }) {
return (
@ -17,13 +16,19 @@ export default function MfaStatus({ onProceed }: { onProceed: () => void }) {
<span className={icons.lock} />
</div>
<p className={`${styles.description} ${mfaStyles.mfaTitle}`}>
<Message {...messages.mfaEnabledForYourAcc} />
<Message
key="mfaEnabledForYourAcc"
defaultMessage="Twofactor authentication for your account is active now"
/>
</p>
</div>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.mfaLoginFlowDesc} />
<Message
key="mfaLoginFlowDesc"
defaultMessage="Additional code will be requested next time you log in. Please note, that Minecraft authorization won't work when twofactor auth is enabled."
/>
</p>
</div>
@ -36,7 +41,7 @@ export default function MfaStatus({ onProceed }: { onProceed: () => void }) {
onProceed();
}}
>
<Message {...messages.disable} />
<Message key="disable" defaultMessage="Disable" />
</a>
</p>
</div>

View File

@ -1,25 +0,0 @@
{
"mfaTitle": "Twofactor authentication",
"mfaDescription": "Twofactor authentication is an extra layer of security designed to ensure you that you're the only person who can access your account, even if the password gets stolen.",
"mfaIntroduction": "First of all, you need to install one of our suggested apps on your phone. This app will generate login codes for you. Please choose your OS to get corresponding installation links.",
"installOnOfTheApps": "Install one of the following apps:",
"findAlternativeApps": "Find alternative apps",
"theAppIsInstalled": "App has been installed",
"scanQrCode": "Open your favorite QR scanner app and scan the following QR code:",
"or": "OR",
"enterKeyManually": "If you can't scan QR code, try entering your secret key manually:",
"whenKeyEntered": "If a temporary code appears in your twofactor auth app, then you may proceed to the next step.",
"ready": "Ready",
"codePlaceholder": "Enter the code here",
"enterCodeFromApp": "In order to finish twofactor auth setup, please enter the code received in the mobile app:",
"enable": "Enable",
"disable": "Disable",
"mfaEnabledForYourAcc": "Twofactor authentication for your account is active now",
"mfaLoginFlowDesc": "Additional code will be requested next time you log in. Please note, that Minecraft authorization won't work when twofactor auth is enabled.",
"disableMfa": "Disable twofactor authentication",
"disableMfaInstruction": "In order to disable twofactor authentication, you need to provide a code from your mobile app and confirm your action with your current account password."
}

View File

@ -7,7 +7,6 @@ import { FormModel } from 'app/components/ui/form';
import MfaEnable, { MfaStep } from './MfaEnable';
import MfaDisable from './MfaDisable';
import messages from './MultiFactorAuth.intl.json';
class MultiFactorAuth extends React.Component<{
step: MfaStep;
@ -25,7 +24,7 @@ class MultiFactorAuth extends React.Component<{
<div className={styles.form}>
<div className={styles.formBody}>
<Message {...messages.mfaTitle}>
<Message key="mfaTitle" defaultMessage="Twofactor authentication">
{(pageTitle) => (
<h3 className={styles.title}>
<Helmet title={pageTitle as string} />
@ -36,7 +35,10 @@ class MultiFactorAuth extends React.Component<{
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.mfaDescription} />
<Message
key="mfaDescription"
defaultMessage="Twofactor authentication is an extra layer of security designed to ensure you that you're the only person who can access your account, even if the password gets stolen."
/>
</p>
</div>
</div>

View File

@ -1 +0,0 @@
export { default } from './Confirmation';

View File

@ -0,0 +1,12 @@
/**
* Extract this file to the level upper to keep the messages prefix. Will be resolved later.
*/
import { defineMessages } from 'react-intl';
export default defineMessages({
mfaIntroduction:
'First of all, you need to install one of our suggested apps on your phone. This app will generate login codes for you. Please choose your OS to get corresponding installation links.',
installOnOfTheApps: 'Install one of the following apps:',
findAlternativeApps: 'Find alternative apps',
});

View File

@ -3,7 +3,7 @@ import { FormattedMessage as Message } from 'react-intl';
import clsx from 'clsx';
import profileForm from '../../profileForm.scss';
import messages from '../MultiFactorAuth.intl.json';
import messages from '../instructions.intl';
import OsInstruction from './OsInstruction';
import OsTile from './OsTile';
import styles from './instructions.scss';

View File

@ -1,7 +1,7 @@
import React from 'react';
import { FormattedMessage as Message } from 'react-intl';
import messages from '../MultiFactorAuth.intl.json';
import messages from '../instructions.intl';
import styles from './instructions.scss';
type OS = 'android' | 'ios' | 'windows';

View File

@ -0,0 +1,12 @@
/**
* Extract this file to the level upper to keep the messages prefix. Will be resolved later.
*/
import { defineMessages } from 'react-intl';
export default defineMessages({
scanQrCode: 'Open your favorite QR scanner app and scan the following QR code:',
or: 'OR',
enterKeyManually: "If you can't scan QR code, try entering your secret key manually:",
whenKeyEntered: 'If a temporary code appears in your twofactor auth app, then you may proceed to the next step.',
});

View File

@ -4,7 +4,7 @@ import { FormattedMessage as Message } from 'react-intl';
import { ImageLoader } from 'app/components/ui/loader';
import profileForm from 'app/components/profile/profileForm.scss';
import messages from '../MultiFactorAuth.intl.json';
import messages from '../keyForm.intl';
import styles from './key-form.scss';
export default function KeyForm({ secret, qrCodeSrc }: { secret: string; qrCodeSrc: string }) {

View File

@ -1,5 +0,0 @@
{
"title": "Confirm your action",
"description": "To complete action enter the account password",
"continue": "Continue"
}

View File

@ -1,12 +1,15 @@
import React, { ComponentType } from 'react';
import { FormattedMessage as Message } from 'react-intl';
import { defineMessages, FormattedMessage as Message } from 'react-intl';
import clsx from 'clsx';
import { Form, Button, Input, FormModel } from 'app/components/ui/form';
import popupStyles from 'app/components/ui/popup/popup.scss';
import styles from './passwordRequestForm.scss';
import messages from './PasswordRequestForm.intl.json';
const labels = defineMessages({
continue: 'Continue',
});
interface Props {
form: FormModel;
@ -19,7 +22,7 @@ const PasswordRequestForm: ComponentType<Props> = ({ form, onSubmit }) => (
<Form onSubmit={onSubmit} form={form}>
<div className={popupStyles.header}>
<h2 className={popupStyles.headerTitle}>
<Message {...messages.title} />
<Message key="title" defaultMessage="Confirm your action" />
</h2>
</div>
@ -27,7 +30,7 @@ const PasswordRequestForm: ComponentType<Props> = ({ form, onSubmit }) => (
<span className={styles.lockIcon} />
<div className={styles.description}>
<Message {...messages.description} />
<Message key="description" defaultMessage="To complete action enter the account password" />
</div>
<Input
@ -40,7 +43,7 @@ const PasswordRequestForm: ComponentType<Props> = ({ form, onSubmit }) => (
center
/>
</div>
<Button color="green" label={messages.continue} block type="submit" />
<Button color="green" label={labels.continue} block type="submit" />
</Form>
</div>
</div>

View File

@ -1,5 +0,0 @@
{
"criticalErrorHappened": "There was a critical error due to which the application can not continue its normal operation.",
"reloadPageOrContactUs": "Please reload this page and try again. If problem occurs again, please report it to the developers by sending email to",
"alsoYouCanInteractWithBackground": "You can also play around with the background it's interactable ;)"
}

View File

@ -1,12 +1,11 @@
import React, { ComponentType } from 'react';
import { FormattedMessage as Message } from 'react-intl';
import appInfo from 'app/components/auth/appInfo/AppInfo.intl.json';
import appName from 'app/components/auth/appInfo/appName.intl';
import BoxesField from './BoxesField';
import styles from './styles.scss';
import messages from './BSoD.intl.json';
interface State {
lastEventId?: string | void;
@ -31,19 +30,28 @@ const BSoD: ComponentType<Props> = ({ lastEventId }) => {
<div className={styles.wrapper}>
<div className={styles.title}>
<Message {...appInfo.appName} />
<Message {...appName} />
</div>
<div className={styles.lineWithMargin}>
<Message {...messages.criticalErrorHappened} />
<Message
key="criticalErrorHappened"
defaultMessage="There was a critical error due to which the application can not continue its normal operation."
/>
</div>
<div className={styles.line}>
<Message {...messages.reloadPageOrContactUs} />
<Message
key="reloadPageOrContactUs"
defaultMessage="Please reload this page and try again. If problem occurs again, please report it to the developers by sending email to"
/>
</div>
<a href={emailUrl} className={styles.support}>
support@ely.by
</a>
<div className={styles.easterEgg}>
<Message {...messages.alsoYouCanInteractWithBackground} />
<Message
key="alsoYouCanInteractWithBackground"
defaultMessage="You can also play around with the background it's interactable ;)"
/>
</div>
</div>
</div>

View File

@ -1,3 +0,0 @@
{
"logout": "Logout"
}

View File

@ -1,4 +0,0 @@
{
"register": "Join",
"login": "Sign in"
}

View File

@ -4,7 +4,6 @@ import { FormattedMessage as Message } from 'react-intl';
import { Account } from 'app/components/accounts/reducer';
import buttons from 'app/components/ui/buttons.scss';
import messages from './Userbar.intl.json';
import styles from './userbar.scss';
import LoggedInPanel from './LoggedInPanel';
@ -27,7 +26,7 @@ export default class Userbar extends Component<{
case 'login':
guestAction = (
<Link to="/login" className={buttons.blue}>
<Message {...messages.login} />
<Message key="login" defaultMessage="Sign in" />
</Link>
);
break;
@ -35,7 +34,7 @@ export default class Userbar extends Component<{
default:
guestAction = (
<Link to="/register" className={buttons.blue}>
<Message {...messages.register} />
<Message key="register" defaultMessage="Join" />
</Link>
);
break;

3
packages/app/i18n/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
*
!index.ts
!.gitignore

Some files were not shown because too many files have changed in this diff Show More