Skip to content

Commit

Permalink
umputun#10 add localization
Browse files Browse the repository at this point in the history
  • Loading branch information
Mavrin committed Feb 17, 2020
1 parent 11327ce commit 8cf36b8
Show file tree
Hide file tree
Showing 22 changed files with 972 additions and 100 deletions.
1 change: 1 addition & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.env
extracted-messages
2 changes: 2 additions & 0 deletions frontend/app/common/config-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ export interface CommentsConfig {
theme?: Theme;
page_title?: string;
node?: string;
locale?: string;
}

export interface LastCommentsConfig {
host: string;
site_id: string;
max_last_comments: number;
locale?: string;
}
83 changes: 52 additions & 31 deletions frontend/app/components/auth-panel/auth-panel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Button } from '@app/components/button';
import { User, PostInfo } from '@app/common/types';

import { Props, AuthPanel } from './auth-panel';
import { IntlProvider } from 'react-intl';

const DefaultProps: Partial<Props> = {
sort: '-score',
Expand All @@ -22,7 +23,11 @@ const DefaultProps: Partial<Props> = {
describe('<AuthPanel />', () => {
describe('For not authorized user', () => {
it('should render login form with google and github provider', () => {
const element = mount(<AuthPanel {...(DefaultProps as Props)} user={null} />);
const element = mount(
<IntlProvider locale="en">
<AuthPanel {...(DefaultProps as Props)} user={null} />
</IntlProvider>
);

const authPanelColumn = element.find('.auth-panel__column');

Expand All @@ -41,12 +46,14 @@ describe('<AuthPanel />', () => {
describe('sorting', () => {
it('should place selected provider first', () => {
const element = mount(
<AuthPanel
{...(DefaultProps as Props)}
providers={['google', 'github', 'yandex']}
provider={{ name: 'github' }}
user={null}
/>
<IntlProvider locale="en">
<AuthPanel
{...(DefaultProps as Props)}
providers={['google', 'github', 'yandex']}
provider={{ name: 'github' }}
user={null}
/>
</IntlProvider>
);

const providerLinks = element
Expand All @@ -61,12 +68,14 @@ describe('<AuthPanel />', () => {

it('should do nothing if provider not found', () => {
const element = mount(
<AuthPanel
{...(DefaultProps as Props)}
providers={['google', 'github', 'yandex']}
provider={{ name: 'baidu' }}
user={null}
/>
<IntlProvider locale="en">
<AuthPanel
{...(DefaultProps as Props)}
providers={['google', 'github', 'yandex']}
provider={{ name: 'baidu' }}
user={null}
/>
</IntlProvider>
);

const providerLinks = element
Expand All @@ -82,11 +91,13 @@ describe('<AuthPanel />', () => {

it('should render login form with google and github provider for read-only post', () => {
const element = mount(
<AuthPanel
{...(DefaultProps as Props)}
user={null}
postInfo={{ ...DefaultProps.postInfo, read_only: true } as PostInfo}
/>
<IntlProvider locale="en">
<AuthPanel
{...(DefaultProps as Props)}
user={null}
postInfo={{ ...DefaultProps.postInfo, read_only: true } as PostInfo}
/>
</IntlProvider>
);

const authPanelColumn = element.find('.auth-panel__column');
Expand All @@ -105,11 +116,13 @@ describe('<AuthPanel />', () => {

it('should not render settings if there is no hidden users', () => {
const element = mount(
<AuthPanel
{...(DefaultProps as Props)}
user={null}
postInfo={{ ...DefaultProps.postInfo, read_only: true } as PostInfo}
/>
<IntlProvider locale="en">
<AuthPanel
{...(DefaultProps as Props)}
user={null}
postInfo={{ ...DefaultProps.postInfo, read_only: true } as PostInfo}
/>
</IntlProvider>
);

const adminAction = element.find('.auth-panel__admin-action');
Expand All @@ -119,12 +132,14 @@ describe('<AuthPanel />', () => {

it('should render settings if there is some hidden users', () => {
const element = mount(
<AuthPanel
{...(DefaultProps as Props)}
user={null}
postInfo={{ ...DefaultProps.postInfo, read_only: true } as PostInfo}
hiddenUsers={{ hidden_joe: {} as any }}
/>
<IntlProvider locale="en">
<AuthPanel
{...(DefaultProps as Props)}
user={null}
postInfo={{ ...DefaultProps.postInfo, read_only: true } as PostInfo}
hiddenUsers={{ hidden_joe: {} as any }}
/>
</IntlProvider>
);

const adminAction = element.find('.auth-panel__admin-action');
Expand All @@ -134,7 +149,11 @@ describe('<AuthPanel />', () => {
});
describe('For authorized user', () => {
it('should render info about current user', () => {
const element = mount(<AuthPanel {...(DefaultProps as Props)} user={{ id: `john`, name: 'John' } as User} />);
const element = mount(
<IntlProvider locale="en">
<AuthPanel {...(DefaultProps as Props)} user={{ id: `john`, name: 'John' } as User} />
</IntlProvider>
);

const authPanelColumn = element.find('.auth-panel__column');

Expand All @@ -148,7 +167,9 @@ describe('<AuthPanel />', () => {
describe('For admin user', () => {
it('should render admin action', () => {
const element = mount(
<AuthPanel {...(DefaultProps as Props)} user={{ id: `test`, admin: true, name: 'John' } as User} />
<IntlProvider locale="en">
<AuthPanel {...(DefaultProps as Props)} user={{ id: `test`, admin: true, name: 'John' } as User} />{' '}
</IntlProvider>
);

const adminAction = element.find('.auth-panel__admin-action').first();
Expand Down
7 changes: 4 additions & 3 deletions frontend/app/components/auth-panel/auth-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { StoreState } from '@app/store';
import { ProviderState } from '@app/store/provider/reducers';
import { Dropdown, DropdownItem } from '@app/components/dropdown';
import { Button } from '@app/components/button';
import { FormattedMessage } from 'react-intl';

import { AnonymousLoginForm } from './__anonymous-login-form';
import { EmailLoginFormConnected } from './__email-login-form';
Expand Down Expand Up @@ -155,7 +156,7 @@ export class AuthPanel extends Component<Props, State> {

return (
<div className="auth-panel__column">
You logged in as{' '}
<FormattedMessage id="authPanel.logged-as" defaultMessage="You logged in as" />{' '}
<Dropdown title={user.name} titleClass="auth-panel__user-dropdown-title" theme={theme}>
<DropdownItem separator={!isUserAnonymous}>
<div
Expand All @@ -176,7 +177,7 @@ export class AuthPanel extends Component<Props, State> {
)}
</Dropdown>{' '}
<Button kind="link" theme={theme} onClick={onSignOut}>
Logout?
<FormattedMessage id="authPanel.logout" defaultMessage="Logout?" />
</Button>
</div>
);
Expand Down Expand Up @@ -263,7 +264,7 @@ export class AuthPanel extends Component<Props, State> {

return (
<div className="auth-panel__column">
{'Login: '}
<FormattedMessage id="authPanel.login" defaultMessage="Login:" />{' '}
{!isAboveThreshold &&
sortedProviders.map((provider, i) => {
const comma = i === 0 ? '' : i === sortedProviders.length - 1 ? ' or ' : ', ';
Expand Down
18 changes: 10 additions & 8 deletions frontend/app/components/comment-form/comment-form.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/** @jsx createElement */
import { createElement, Component, createRef, Fragment } from 'preact';
import { FormattedMessage } from 'react-intl';
import b, { Mix } from 'bem-react-helper';

import { User, Theme, Image, ApiError } from '@app/common/types';
Expand Down Expand Up @@ -51,12 +52,6 @@ interface State {
buttonText: null | string;
}

const Labels = {
main: 'Send',
edit: 'Save',
reply: 'Reply',
};

const ImageMimeRegex = /image\//i;

export class CommentForm extends Component<Props, State> {
Expand Down Expand Up @@ -343,6 +338,11 @@ export class CommentForm extends Component<Props, State> {
render(props: Props, { isDisabled, isErrorShown, errorMessage, preview, maxLength, text, buttonText }: State) {
const charactersLeft = maxLength - text.length;
errorMessage = props.errorMessage || errorMessage;
const Labels = {
main: <FormattedMessage id="commentForm.send" defaultMessage="Send" />,
edit: <FormattedMessage id="commentForm.save" defaultMessage="Save" />,
reply: <FormattedMessage id="commentForm.replay" defaultMessage="Replay" />,
};
const label = buttonText || Labels[props.mode || 'main'];

return (
Expand Down Expand Up @@ -406,7 +406,7 @@ export class CommentForm extends Component<Props, State> {
disabled={isDisabled}
onClick={this.getPreview}
>
Preview
<FormattedMessage id="commentForm.preview" defaultMessage="Preview" />
</Button>
)}
<Button kind="primary" size="large" mix="comment-form__button" type="submit" disabled={isDisabled}>
Expand Down Expand Up @@ -439,7 +439,9 @@ export class CommentForm extends Component<Props, State> {
!!preview && (
<div className="comment-form__preview-wrapper">
<div
className={b('comment-form__preview', { mix: b('raw-content', {}, { theme: props.theme }) })}
className={b('comment-form__preview', {
mix: b('raw-content', {}, { theme: props.theme }),
})}
dangerouslySetInnerHTML={{ __html: preview }}
/>
</div>
Expand Down
31 changes: 28 additions & 3 deletions frontend/app/components/comment/comment.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,23 @@ import { Props, Comment } from './comment';
import { User, Comment as CommentType, PostInfo } from '@app/common/types';
import { sleep } from '@app/utils/sleep';
import { StaticStore } from '@app/common/static_store';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';

jest.mock('react-intl');
(defineMessages as any).mockImplementation(() => '');
(useIntl as any).mockImplementation(() => ({ formatDate: jest.fn(), formatTime: jest.fn() }));
(FormattedMessage as any).mockImplementation(({ defaultMessage }: { defaultMessage: any }) => {
return defaultMessage;
});

const intl: any = {
formatMessage() {
return '';
},
};

const DefaultProps: Partial<Props> = {
intl,
post_info: {
read_only: false,
} as PostInfo,
Expand Down Expand Up @@ -214,7 +229,12 @@ describe('<Comment />', () => {
expect(controls.length).toBe(5);
expect(controls.at(0).text()).toEqual('Copy');
expect(controls.at(1).text()).toEqual('Pin');
expect(controls.at(2).text()).toEqual('Hide');
expect(
controls
.at(2)
.find('FormattedMessage')
.props()
).toEqual(expect.objectContaining({ defaultMessage: 'Hide' }));
expect(controls.at(3).getDOMNode().childNodes[0].textContent).toEqual('Block');
expect(controls.at(4).text()).toEqual('Delete');
});
Expand All @@ -226,7 +246,12 @@ describe('<Comment />', () => {

const controls = element.find('.comment__controls').children();
expect(controls.length).toBe(1);
expect(controls.at(0).text()).toEqual('Hide');
expect(
controls
.at(0)
.find('FormattedMessage')
.props()
).toEqual(expect.objectContaining({ defaultMessage: 'Hide' }));
});

it('verification badge clickable for admin', () => {
Expand Down Expand Up @@ -254,7 +279,7 @@ describe('<Comment />', () => {

it('should be editable', () => {
const initTime = new Date().toString();
const changedTime = new Date(Date.now() + 10 * 1000).toString();
const changedTime = new Date(Date.now() + 10 * 1000);
const props: Partial<Props> = {
...DefaultProps,
user: DefaultProps.user as User,
Expand Down
42 changes: 29 additions & 13 deletions frontend/app/components/comment/comment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,17 @@ import Countdown from '@app/components/countdown';
import { boundActions } from './connected-comment';
import { getPreview, uploadImage } from '@app/common/api';
import postMessage from '@app/utils/postMessage';
import { FormattedMessage, useIntl, IntlShape, defineMessages } from 'react-intl';

defineMessages({
'comment.delete': {
id: 'comment.delete',
defaultMessage: 'Do you want to delete this comment?',
},
});

export type Props = {
intl: IntlShape;
user: User | null;
data: CommentType;
repliesCount?: number;
Expand Down Expand Up @@ -219,7 +228,11 @@ export class Comment extends Component<Props, State> {
};

deleteComment = () => {
if (confirm('Do you want to delete this comment?')) {
const deleteComment = this.props.intl.formatMessage({
id: 'comment.delete',
defaultMessage: 'comment.delete',
});
if (confirm(deleteComment)) {
this.props.setReplyEditState!({ id: this.props.data.id, state: CommentMode.None });

this.props.removeComment!(this.props.data.id);
Expand Down Expand Up @@ -405,7 +418,7 @@ export class Comment extends Component<Props, State> {
if (!isCurrentUser) {
controls.push(
<Button kind="link" {...getHandleClickProps(this.hideUser)} mix="comment__control">
Hide
<FormattedMessage id="comment.hide" defaultMessage="Hide" />
</Button>
);
}
Expand Down Expand Up @@ -476,7 +489,7 @@ export class Comment extends Component<Props, State> {
: props.data.delete
? 'This comment was deleted'
: props.data.text,
time: formatTime(new Date(props.data.time)),
time: new Date(props.data.time),
orig: isEditing
? props.data.orig &&
props.data.orig.replace(/&[#A-Za-z0-9]+;/gi, entity => {
Expand Down Expand Up @@ -602,7 +615,7 @@ export class Comment extends Component<Props, State> {
)}

<a href={`${o.locator.url}#${COMMENT_NODE_CLASSNAME_PREFIX}${o.id}`} className="comment__time">
{o.time}
<FormatTime time={o.time} />
</a>

{!!props.level && props.level > 0 && props.view === 'main' && (
Expand Down Expand Up @@ -764,13 +777,16 @@ function getTextSnippet(html: string) {
return snippet.length === LENGTH && result.length !== LENGTH ? `${snippet}...` : snippet;
}

function formatTime(time: Date) {
// 'ru-RU' adds a dot as a separator
const date = time.toLocaleDateString(['ru-RU'], { day: '2-digit', month: '2-digit', year: '2-digit' });

// do it manually because Intl API doesn't add leading zeros to hours; idk why
const hours = `0${time.getHours()}`.slice(-2);
const mins = `0${time.getMinutes()}`.slice(-2);

return `${date} at ${hours}:${mins}`;
function FormatTime({ time }: { time: Date }) {
const intl = useIntl();
return (
<FormattedMessage
id="comment.time"
defaultMessage="{day} at {time}"
values={{
day: intl.formatDate(time),
time: intl.formatTime(time),
}}
/>
);
}
Loading

0 comments on commit 8cf36b8

Please sign in to comment.