Add internationalization (#114)

🇬🇧 and 🇮🇹
This commit is contained in:
Riccardo Graziosi
2022-06-05 11:40:43 +02:00
committed by GitHub
parent ba86e81aa0
commit 78049a820c
71 changed files with 802 additions and 266 deletions

View File

@@ -1,4 +1,5 @@
import * as React from 'react';
import I18n from 'i18n-js';
import NewPostForm from './NewPostForm';
import Spinner from '../shared/Spinner';
@@ -86,7 +87,7 @@ class NewPost extends React.Component<Props, State> {
if (title === '') {
this.setState({
error: 'You forgot to enter a title!',
error: I18n.t('board.new_post.no_title'),
isLoading: false,
});
return;
@@ -109,7 +110,7 @@ class NewPost extends React.Component<Props, State> {
if (res.status === HttpStatus.Created) {
this.setState({
success: 'Post published! You will be redirected soon...',
success: I18n.t('board.new_post.submit_success'),
title: '',
description: '',
@@ -124,7 +125,7 @@ class NewPost extends React.Component<Props, State> {
} catch (e) {
this.setState({
error: 'An unknown error occurred, try again.'
error: I18n.t('board.new_post.submit_error')
});
}
}
@@ -151,11 +152,16 @@ class NewPost extends React.Component<Props, State> {
onClick={this.toggleForm}
className="submitBtn"
outline={showForm}>
{ showForm ? 'Cancel' : 'Submit feedback' }
{
showForm ?
I18n.t('board.new_post.cancel_button')
:
I18n.t('board.new_post.submit_button')
}
</Button>
:
<a href="/users/sign_in" className="btn btn-dark">
Log in / Sign up
{I18n.t('board.new_post.login_button')}
</a>
}

View File

@@ -1,4 +1,5 @@
import * as React from 'react';
import I18n from 'i18n-js';
import Button from '../shared/Button';
@@ -20,7 +21,7 @@ const NewPostForm = ({
<div className="newPostForm">
<form>
<div className="form-group">
<label htmlFor="postTitle">Title</label>
<label htmlFor="postTitle">{I18n.t('board.new_post.title')}</label>
<input
type="text"
value={title}
@@ -33,7 +34,7 @@ const NewPostForm = ({
/>
</div>
<div className="form-group">
<label htmlFor="postDescription">Description (optional)</label>
<label htmlFor="postDescription">{I18n.t('board.new_post.description')}</label>
<textarea
value={description}
onChange={e => handleDescriptionChange(e.target.value)}
@@ -44,7 +45,7 @@ const NewPostForm = ({
></textarea>
</div>
<Button onClick={e => handleSubmit(e)} className="submitBtn d-block mx-auto">
Submit feedback
{I18n.t('board.new_post.submit_button')}
</Button>
</form>
</div>

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import I18n from 'i18n-js';
import InfiniteScroll from 'react-infinite-scroller';
import PostListItem from './PostListItem';
@@ -64,7 +64,7 @@ const PostList = ({
/>
))
:
areLoading ? <p></p> : <CenteredMutedText>There are no posts.</CenteredMutedText>
areLoading ? <p></p> : <CenteredMutedText>{I18n.t('board.posts_list.empty')}</CenteredMutedText>
}
</InfiniteScroll>
</div>

View File

@@ -1,4 +1,5 @@
import * as React from 'react';
import I18n from 'i18n-js';
import PostStatusListItem from './PostStatusListItem';
import Spinner from '../shared/Spinner';
@@ -24,7 +25,7 @@ const PostStatusFilter = ({
currentFilter,
}: Props) => (
<div className="postStatusFilterContainer sidebarCard">
<BoxTitleText>Filter by status</BoxTitleText>
<BoxTitleText>{I18n.t('board.filter_box.title')}</BoxTitleText>
{
postStatuses.map((postStatus, i) => (
<PostStatusListItem

View File

@@ -1,4 +1,5 @@
import * as React from 'react';
import I18n from 'i18n-js';
import { BoxTitleText } from '../shared/CustomTexts';
@@ -9,7 +10,7 @@ interface Props {
const SearchFilter = ({ searchQuery, handleChange }: Props) => (
<div className="sidebarCard">
<BoxTitleText>Search</BoxTitleText>
<BoxTitleText>{I18n.t('board.search_box.title')}</BoxTitleText>
<input
type="search"

View File

@@ -1,4 +1,5 @@
import * as React from 'react';
import I18n from 'i18n-js';
import Gravatar from 'react-gravatar';
import NewComment from './NewComment';
@@ -50,12 +51,24 @@ const Comment = ({
<div className="commentHeader">
<Gravatar email={userEmail} size={28} className="gravatar" />
<span className="commentAuthor">{userFullName}</span>
{ isPostUpdate ? <span className="postUpdateBadge">Post update</span> : null }
{
isPostUpdate ?
<span className="postUpdateBadge">
{I18n.t('post.comments.post_update_badge')}
</span>
:
null
}
</div>
<p className="commentBody">{body}</p>
<div className="commentFooter">
<a className="commentReplyButton commentLink" onClick={handleToggleCommentReply}>
{ replyForm.isOpen ? 'Cancel' : 'Reply' }
{
replyForm.isOpen ?
I18n.t('common.buttons.cancel')
:
I18n.t('post.comments.reply_button')
}
</a>
{
isPowerUser ?
@@ -68,14 +81,18 @@ const Comment = ({
{ 'Post update: ' + (isPostUpdate ? 'yes' : 'no') }
</a>
<Separator />
<a href={`/admin/comments/${id}/edit`} className="commentLink" data-turbolinks="false">Edit</a>
<a href={`/admin/comments/${id}/edit`} className="commentLink" data-turbolinks="false">
{I18n.t('common.buttons.edit')}
</a>
<Separator />
<a
href={`/admin/comments/${id}`}
className="commentLink"
data-method="delete"
data-confirm="Are you sure?"
data-turbolinks="false">Delete</a>
data-turbolinks="false">
{I18n.t('common.buttons.delete')}
</a>
</React.Fragment>
:

View File

@@ -1,4 +1,5 @@
import * as React from 'react';
import I18n from 'i18n-js';
import NewComment from './NewComment';
import CommentList from './CommentList';
@@ -7,6 +8,7 @@ import { DangerText } from '../shared/CustomTexts';
import IComment from '../../interfaces/IComment';
import { ReplyFormState } from '../../reducers/replyFormReducer';
import Separator from '../shared/Separator';
interface Props {
postId: number;
@@ -106,7 +108,9 @@ class CommentsP extends React.Component<Props> {
{ error ? <DangerText>{error}</DangerText> : null }
<div className="commentsTitle">
activity &bull; {comments.length} comment{comments.length === 1 ? '' : 's'}
{I18n.t('post.comments.title')}
<Separator />
{I18n.t('common.comments_number', { count: comments.length })}
</div>
<CommentList

View File

@@ -1,4 +1,5 @@
import * as React from 'react';
import I18n from 'i18n-js';
import Gravatar from 'react-gravatar';
import NewCommentUpdateSection from './NewCommentUpdateSection';
@@ -50,13 +51,13 @@ const NewComment = ({
<textarea
value={body}
onChange={handleChange}
placeholder="Leave a comment"
placeholder={I18n.t('post.new_comment.body_placeholder')}
className="newCommentBody"
/>
<Button
onClick={() => handleSubmit(body, parentId, postUpdateFlagValue)}
className="submitCommentButton">
{ isSubmitting ? <Spinner color="light" /> : 'Submit' }
{ isSubmitting ? <Spinner color="light" /> : I18n.t('post.new_comment.submit_button') }
</Button>
</div>
{
@@ -70,9 +71,12 @@ const NewComment = ({
}
</React.Fragment>
:
<a href="/users/sign_in" className="loginInfo">You need to log in to post comments.</a>
<a href="/users/sign_in" className="loginInfo">
{I18n.t('post.new_comment.not_logged_in')}
</a>
}
</div>
{ error ? <DangerText>{error}</DangerText> : null }
</React.Fragment>
);

View File

@@ -1,4 +1,6 @@
import * as React from 'react';
import I18n from 'i18n-js';
import { MutedText } from '../shared/CustomTexts';
interface Props {
@@ -19,11 +21,11 @@ const NewCommentUpdateSection = ({
checked={postUpdateFlagValue || false}
/>
&nbsp;
<label htmlFor="isPostUpdateFlag">Mark as post update</label>
<label htmlFor="isPostUpdateFlag">{I18n.t('post.new_comment.is_post_update')}</label>
</div>
{
postUpdateFlagValue ?
<MutedText>Users that follow this post will be notified</MutedText>
<MutedText>{I18n.t('post.new_comment.user_notification')}</MutedText>
:
null
}

View File

@@ -1,6 +1,7 @@
import * as React from 'react';
import Button from '../shared/Button';
import I18n from 'i18n-js';
import Button from '../shared/Button';
import { BoxTitleText, SmallMutedText } from '../shared/CustomTexts';
interface Props {
@@ -13,17 +14,17 @@ interface Props {
const ActionBox = ({followed, submitFollow, isLoggedIn}: Props) => (
<div className="actionBoxContainer">
<div className="actionBoxFollow">
<BoxTitleText>Actions</BoxTitleText>
<BoxTitleText>{I18n.t('post.action_box.title')}</BoxTitleText>
<br />
<Button onClick={isLoggedIn ? submitFollow : () => location.href = '/users/sign_in'} outline>
{ followed ? 'Unfollow post' : 'Follow post' }
{ followed ? I18n.t('post.action_box.unfollow_button') : I18n.t('post.action_box.follow_button') }
</Button>
<br />
<SmallMutedText>
{ followed ?
'you\'re receiving notifications about new updates on this post'
I18n.t('post.action_box.following_description')
:
'you won\'t receive notifications about this post'
I18n.t('post.action_box.not_following_description')
}
</SmallMutedText>
</div>

View File

@@ -1,4 +1,5 @@
import * as React from 'react';
import I18n from 'i18n-js';
import Gravatar from 'react-gravatar';
import ILike from '../../interfaces/ILike';
@@ -17,11 +18,13 @@ interface Props {
const LikeList = ({ likes, areLoading, error}: Props) => (
<div className="likeListContainer">
<BoxTitleText>Likes</BoxTitleText>
<BoxTitleText>{I18n.t('post.likes_box.title')}</BoxTitleText>
{ areLoading ? <Spinner /> : null }
{ error ? <DangerText>{error}</DangerText> : null }
<div className="likeList">
{ likes.length === 0 ? <CenteredMutedText>There are no likes yet.</CenteredMutedText> : null }
{ likes.length === 0 ? <CenteredMutedText>{I18n.t('post.likes_box.empty')}</CenteredMutedText> : null }
{
likes.map((like, i) => (
<div className="likeListItem" key={i}>

View File

@@ -1,4 +1,5 @@
import * as React from 'react';
import I18n from 'i18n-js';
import IPost from '../../interfaces/IPost';
import IPostStatus from '../../interfaces/IPostStatus';
@@ -135,7 +136,11 @@ class PostP extends React.Component<Props> {
<h2>{post.title}</h2>
{
isPowerUser && post ?
<a href={`/admin/posts/${post.id}`} data-turbolinks="false">Edit</a> : null
<a href={`/admin/posts/${post.id}`} data-turbolinks="false">
{I18n.t('post.edit_button')}
</a>
:
null
}
</div>
{

View File

@@ -1,4 +1,5 @@
import * as React from 'react';
import I18n from 'i18n-js';
import IPostStatus from '../../interfaces/IPostStatus';
@@ -35,7 +36,7 @@ const PostStatusSelect = ({
))}
</optgroup>
<optgroup label="No post status">
<option value={NO_POST_STATUS_VALUE}>None</option>
<option value={NO_POST_STATUS_VALUE}>{I18n.t('post.post_status_select.no_post_status')}</option>
</optgroup>
</select>
);

View File

@@ -1,4 +1,5 @@
import * as React from 'react';
import I18n from 'i18n-js';
import Gravatar from 'react-gravatar';
import { BoxTitleText, DangerText, CenteredMutedText, MutedText } from '../shared/CustomTexts';
@@ -25,11 +26,18 @@ const PostUpdateList = ({
error,
}: Props) => (
<div className="postUpdateListContainer">
<BoxTitleText>Updates</BoxTitleText>
<BoxTitleText>{I18n.t('post.updates_box.title')}</BoxTitleText>
{ areLoading ? <Spinner /> : null }
{ error ? <DangerText>{error}</DangerText> : null }
<div className="postUpdateList">
{ postUpdates.length === 0 ? <CenteredMutedText>There are no updates yet.</CenteredMutedText> : null }
{
postUpdates.length === 0 ?
<CenteredMutedText>{I18n.t('post.updates_box.empty')}</CenteredMutedText>
:
null
}
{
postUpdates.map((postUpdate, i) => (
<div className="postUpdateListItem" key={i}>
@@ -43,7 +51,7 @@ const PostUpdateList = ({
postUpdate.body
:
<React.Fragment>
<i>changed status to</i>&nbsp;
<i>{I18n.t('post.updates_box.status_change')}</i>&nbsp;
<PostStatusLabel
{...postStatuses.find(postStatus => postStatus.id === postUpdate.postStatusId)}
/>

View File

@@ -1,4 +1,5 @@
import * as React from 'react';
import I18n from 'i18n-js';
import { Draggable } from 'react-beautiful-dnd';
import { DescriptionText } from '../../shared/CustomTexts';
@@ -82,7 +83,7 @@ class BoardsEditable extends React.Component<Props, State> {
</div>
<div className="boardEditableActions">
<a onClick={this.toggleEditMode}>Edit</a>
<a onClick={this.toggleEditMode}>{I18n.t('common.buttons.edit')}</a>
<Separator />
@@ -90,7 +91,7 @@ class BoardsEditable extends React.Component<Props, State> {
onClick={() => handleDelete(id)}
data-confirm="Are you sure?"
>
Delete
{I18n.t('common.buttons.delete')}
</a>
</div>
</React.Fragment>
@@ -107,7 +108,7 @@ class BoardsEditable extends React.Component<Props, State> {
<a
className="boardFormCancelButton"
onClick={this.toggleEditMode}>
Cancel
{I18n.t('common.buttons.cancel')}
</a>
</React.Fragment>
}

View File

@@ -1,4 +1,5 @@
import * as React from 'react';
import I18n from 'i18n-js';
import Button from '../../shared/Button';
@@ -83,7 +84,7 @@ class BoardForm extends React.Component<Props, State> {
<div className="boardMandatoryForm">
<input
type="text"
placeholder="Board name"
placeholder={I18n.t('site_settings.boards.form.name')}
value={name}
onChange={e => this.onNameChange(e.target.value)}
className="form-control"
@@ -94,12 +95,17 @@ class BoardForm extends React.Component<Props, State> {
className="newBoardButton"
disabled={!this.isFormValid()}
>
{mode === 'create' ? 'Create' : 'Save'}
{
mode === 'create' ?
I18n.t('common.buttons.create')
:
I18n.t('common.buttons.update')
}
</Button>
</div>
<textarea
placeholder="Optional board description"
placeholder={I18n.t('site_settings.boards.form.description')}
value={description}
onChange={e => this.onDescriptionChange(e.target.value)}
className="form-control"

View File

@@ -1,4 +1,5 @@
import * as React from 'react';
import I18n from 'i18n-js';
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
@@ -90,7 +91,7 @@ class BoardsSiteSettingsP extends React.Component<Props> {
return (
<React.Fragment>
<div className="content">
<h2>Boards</h2>
<h2>{I18n.t('site_settings.boards.title')}</h2>
{
boards.items.length > 0 ?
@@ -121,12 +122,12 @@ class BoardsSiteSettingsP extends React.Component<Props> {
boards.areLoading ?
<Spinner />
:
<CenteredMutedText>There are no boards. Create one below!</CenteredMutedText>
<CenteredMutedText>{I18n.t('site_settings.boards.empty')}</CenteredMutedText>
}
</div>
<div className="content">
<h2>New</h2>
<h2>{I18n.t('site_settings.boards.new')}</h2>
<BoardForm mode='create' handleSubmit={this.handleSubmit} />
</div>

View File

@@ -1,4 +1,5 @@
import * as React from 'react';
import I18n from 'i18n-js';
import { Draggable } from 'react-beautiful-dnd';
@@ -74,7 +75,7 @@ class PostStatusEditable extends React.Component<Props, State> {
<PostStatusLabel name={name} color={color} />
<div className="postStatusEditableActions">
<a onClick={this.toggleEditMode}>Edit</a>
<a onClick={this.toggleEditMode}>{I18n.t('common.buttons.edit')}</a>
<Separator />
@@ -82,7 +83,7 @@ class PostStatusEditable extends React.Component<Props, State> {
onClick={() => handleDelete(id)}
data-confirm="Are you sure?"
>
Delete
{I18n.t('common.buttons.delete')}
</a>
</div>
</React.Fragment>
@@ -99,7 +100,7 @@ class PostStatusEditable extends React.Component<Props, State> {
<a
className="postStatusFormCancelButton"
onClick={this.toggleEditMode}>
Cancel
{I18n.t('common.buttons.cancel')}
</a>
</React.Fragment>
}

View File

@@ -1,4 +1,5 @@
import * as React from 'react';
import I18n from 'i18n-js';
import Button from '../../shared/Button';
@@ -89,7 +90,7 @@ class PostStatusForm extends React.Component<Props, State> {
<div className="postStatusForm">
<input
type="text"
placeholder="Post status name"
placeholder={I18n.t('site_settings.post_statuses.form.name')}
value={name}
onChange={e => this.onNameChange(e.target.value)}
className="form-control"
@@ -107,7 +108,12 @@ class PostStatusForm extends React.Component<Props, State> {
className="newPostStatusButton"
disabled={!this.isFormValid()}
>
{mode === 'create' ? 'Create' : 'Save'}
{
mode === 'create' ?
I18n.t('common.buttons.create')
:
I18n.t('common.buttons.update')
}
</Button>
</div>
);

View File

@@ -1,4 +1,5 @@
import * as React from 'react';
import I18n from 'i18n-js';
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import IPostStatus from '../../../interfaces/IPostStatus';
@@ -85,7 +86,7 @@ class PostStatusesSiteSettingsP extends React.Component<Props> {
return (
<React.Fragment>
<div className="content">
<h2>Post statuses</h2>
<h2>{I18n.t('site_settings.post_statuses.title')}</h2>
{
postStatuses.items.length > 0 ?
@@ -116,12 +117,12 @@ class PostStatusesSiteSettingsP extends React.Component<Props> {
postStatuses.areLoading ?
<Spinner />
:
<CenteredMutedText>There are no post statuses. Create one below!</CenteredMutedText>
<CenteredMutedText>{I18n.t('site_settings.post_statuses.empty')}</CenteredMutedText>
}
</div>
<div className="content">
<h2>New</h2>
<h2>{I18n.t('site_settings.post_statuses.new')}</h2>
<PostStatusForm mode='create' handleSubmit={this.handleSubmit} />
</div>

View File

@@ -1,11 +1,14 @@
import * as React from 'react';
import I18n from 'i18n-js';
interface Props {
number: number;
}
const CommentsNumber = ({ number }: Props) => (
<span className="badge badgeLight">{`${number} comment${number === 1 ? '' : 's'}`}</span>
<span className="badge badgeLight">
{I18n.t('common.comments_number', { count: number })}
</span>
);
export default CommentsNumber;

View File

@@ -1,4 +1,5 @@
import * as React from 'react';
import I18n from 'i18n-js';
interface Props {
name: string;
@@ -10,7 +11,7 @@ const PostStatusLabel = ({
color,
}: Props) => (
<span className="badge" style={{backgroundColor: color || 'black', color: 'white'}}>
{(name || 'no status').toUpperCase()}
{(name || I18n.t('common.no_status')).toUpperCase()}
</span>
);

View File

@@ -1,4 +1,5 @@
import * as React from 'react';
import I18n from 'i18n-js';
import Spinner from './Spinner';
@@ -14,9 +15,11 @@ const SiteSettingsInfoBox = ({ areUpdating, error }: Props) => (
<Spinner />
:
error ?
<span className="error">An error occurred: {JSON.stringify(error)}</span>
<span className="error">
{I18n.t('site_settings.info_box.error', { message: JSON.stringify(error) })}
</span>
:
<span>Everything up to date</span>
<span>{I18n.t('site_settings.info_box.up_to_date')}</span>
}
</div>
);

View File

@@ -1,8 +1,9 @@
import * as React from 'react';
import I18n from 'i18n-js';
const Spinner = ({ color = 'dark' }) => (
<div className={`spinner-grow d-block mx-auto text-${color}`} role="status">
<span className="sr-only">Loading...</span>
<span className="sr-only">{I18n.t('common.loading')}</span>
</div>
);