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

@@ -4,6 +4,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby '2.6.6'
gem 'rails', '6.0.4.7'
gem 'i18n-js'
gem 'pg', '>= 0.18', '< 2.0'

View File

@@ -112,6 +112,8 @@ GEM
activesupport (>= 5.0)
i18n (1.10.0)
concurrent-ruby (~> 1.0)
i18n-js (3.9.2)
i18n (>= 0.6.6)
jbuilder (2.11.5)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
@@ -297,6 +299,7 @@ DEPENDENCIES
capybara (>= 2.15)
devise (= 4.7.3)
factory_bot_rails (~> 5.0.2)
i18n-js
jbuilder (~> 2.7)
kaminari (~> 1.2.1)
listen (>= 3.0.5, < 3.2)

View File

@@ -11,13 +11,13 @@ module Admin
def authenticate_admin
unless user_signed_in?
flash[:alert] = 'You must be logged in to access this page.'
flash[:alert] = t('backend.errors.not_logged_in')
redirect_to new_user_session_path
return
end
unless current_user.admin?
flash[:alert] = 'You do not have the privilegies to access this page.'
flash[:alert] = t('backend.errors.not_enough_privileges')
redirect_to root_path
return
end

View File

@@ -20,7 +20,7 @@ class BoardsController < ApplicationController
render json: board, status: :created
else
render json: {
error: I18n.t('errors.board.create', message: board.errors.full_messages)
error: board.errors.full_messages
}, status: :unprocessable_entity
end
end
@@ -35,7 +35,7 @@ class BoardsController < ApplicationController
print board.errors.full_messages
render json: {
error: I18n.t('errors.board.update', message: board.errors.full_messages)
error: board.errors.full_messages
}, status: :unprocessable_entity
end
end
@@ -49,7 +49,7 @@ class BoardsController < ApplicationController
}, status: :accepted
else
render json: {
error: I18n.t('errors.board.destroy', message: board.errors.full_messages)
error: board.errors.full_messages
}, status: :unprocessable_entity
end
end
@@ -67,7 +67,7 @@ class BoardsController < ApplicationController
render json: workflow_output
else
render json: {
error: I18n.t("errors.board.update_order")
error: t("backend.errors.board.update_order")
}, status: :unprocessable_entity
end
end

View File

@@ -30,7 +30,7 @@ class CommentsController < ApplicationController
), status: :created
else
render json: {
error: I18n.t('errors.comment.create', message: comment.errors.full_messages)
error: comment.errors.full_messages
}, status: :unprocessable_entity
end
end
@@ -41,7 +41,7 @@ class CommentsController < ApplicationController
comment.assign_attributes(comment_params)
if !current_user.power_user? && current_user.id != post.user_id
render json: I18n.t('errors.unauthorized'), status: :unauthorized
render json: t('backend.errors.unauthorized'), status: :unauthorized
return
end
@@ -51,7 +51,7 @@ class CommentsController < ApplicationController
)
else
render json: {
error: I18n.t('errors.comment.update', message: comment.errors.full_messages)
error: comment.errors.full_messages
}, status: :unprocessable_entity
end
end

View File

@@ -20,7 +20,7 @@ class FollowsController < ApplicationController
}, status: :created
else
render json: {
error: I18n.t('errors.follows.create', message: follow.errors.full_messages)
error: follow.errors.full_messages
}, status: :unprocessable_entity
end
end
@@ -37,7 +37,7 @@ class FollowsController < ApplicationController
}, status: :accepted
else
render json: {
error: I18n.t('errors.follow.destroy', message: follow.errors.full_messages)
error: follow.errors.full_messages
}, status: :unprocessable_entity
end
end

View File

@@ -25,7 +25,7 @@ class LikesController < ApplicationController
}, status: :created
else
render json: {
error: I18n.t('errors.likes.create', message: like.errors.full_messages)
error: like.errors.full_messages
}, status: :unprocessable_entity
end
end
@@ -42,7 +42,7 @@ class LikesController < ApplicationController
}, status: :accepted
else
render json: {
error: I18n.t('errors.likes.destroy', message: like.errors.full_messages)
error: like.errors.full_messages
}, status: :unprocessable_entity
end
end

View File

@@ -16,7 +16,7 @@ class PostStatusesController < ApplicationController
render json: post_status, status: :created
else
render json: {
error: I18n.t('errors.post_status.create', message: post_status.errors.full_messages)
error: post_status.errors.full_messages
}, status: :unprocessable_entity
end
end
@@ -29,7 +29,7 @@ class PostStatusesController < ApplicationController
render json: post_status, status: :ok
else
render json: {
error: I18n.t('errors.post_status.update', message: post_status.errors.full_messages)
error: post_status.errors.full_messages
}, status: :unprocessable_entity
end
end
@@ -43,7 +43,7 @@ class PostStatusesController < ApplicationController
}, status: :accepted
else
render json: {
error: I18n.t('errors.post_statuses.destroy', message: post_status.errors.full_messages)
error: post_status.errors.full_messages
}, status: :unprocessable_entity
end
end
@@ -61,7 +61,7 @@ class PostStatusesController < ApplicationController
render json: workflow_output
else
render json: {
error: I18n.t("errors.post_status.update_order")
error: t("backend.errors.post_status.update_order")
}, status: :unprocessable_entity
end
end

View File

@@ -33,7 +33,7 @@ class PostsController < ApplicationController
render json: post, status: :created
else
render json: {
error: I18n.t('errors.post.create', message: post.errors.full_messages)
error: post.errors.full_messages
}, status: :unprocessable_entity
end
end
@@ -54,7 +54,7 @@ class PostsController < ApplicationController
post = Post.find(params[:id])
if !current_user.power_user? && current_user.id != post.user_id
render json: I18n.t('errors.unauthorized'), status: :unauthorized
render json: t('backend.errors.unauthorized'), status: :unauthorized
return
end
@@ -83,7 +83,7 @@ class PostsController < ApplicationController
render json: post, status: :no_content
else
render json: {
error: I18n.t('errors.post.update', message: post.errors.full_messages)
error: post.errors.full_messages
}, status: :unprocessable_entity
end
end

View File

@@ -1,13 +1,13 @@
module ApplicationHelper
def authenticate_admin
unless user_signed_in?
flash[:alert] = 'You must be logged in to access this page.'
flash[:alert] = t('backend.errors.not_logged_in')
redirect_to new_user_session_path
return
end
unless current_user.moderator? || current_user.admin?
flash[:alert] = 'You do not have the privilegies to access this page.'
flash[:alert] = t('backend.errors.not_enough_privileges')
redirect_to root_path
return
end

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>
);

View File

@@ -1,3 +1,5 @@
import I18n from 'i18n-js';
export const friendlyDate = date => {
var now = new Date();
var timeStamp = fromRailsStringToJavascriptDate(date);
@@ -5,16 +7,16 @@ export const friendlyDate = date => {
var secondsPast = (now.getTime() - timeStamp.getTime()) / 1000;
if (secondsPast < 60) {
return 'just now';
return I18n.t('common.datetime.now');
} else if (secondsPast < 3600) {
let minutesPast = Math.round(secondsPast / 60);
return minutesPast + ' ' + (minutesPast === 1 ? 'minute' : 'minutes') + ' ago';
return I18n.t('common.datetime.minutes', { count: minutesPast });
} else if (secondsPast <= 86400) {
let hoursPast = Math.round(secondsPast / 3600);
return hoursPast + ' ' + (hoursPast === 1 ? 'hour' : 'hours') + ' ago';
return I18n.t('common.datetime.hours', { count: hoursPast });
} else {
let daysPast = Math.round(secondsPast / 86400);
return daysPast + ' ' + (daysPast === 1 ? 'day' : 'days') + ' ago';
return I18n.t('common.datetime.days', { count: daysPast });
}
}

View File

@@ -23,3 +23,5 @@ require("../images/logo.png")
var componentRequireContext = require.context("components", true);
var ReactRailsUJS = require("react_ujs");
ReactRailsUJS.useContext(componentRequireContext);
import I18n from 'translations/index.js.erb'

View File

@@ -0,0 +1,3 @@
<% name = 'Erb' %>
console.log('Hello world from <%= name %>')

View File

@@ -0,0 +1,4 @@
import I18n from "i18n-js"
I18n.translations = <%= I18n::JS.filtered_translations.to_json %>
I18n.locale = LOCALE
export default I18n

View File

@@ -1,11 +1,13 @@
class UserMailer < ApplicationMailer
layout 'user_mailer'
def notify_post_owner(comment:)
@comment = comment
@user = comment.post.user
mail(
to: @user.email,
subject: "[#{app_name}] New comment on #{comment.post.title}"
subject: default_i18n_subject(app_name: app_name, post: comment.post.title)
)
end
@@ -15,7 +17,7 @@ class UserMailer < ApplicationMailer
mail(
to: @user.email,
subject: "[#{app_name}] New reply on your comment from #{comment.post.title}"
subject: default_i18n_subject(app_name: app_name, post: comment.post.title)
)
end
@@ -24,7 +26,7 @@ class UserMailer < ApplicationMailer
mail(
to: comment.post.followers.pluck(:email),
subject: "[#{app_name}] New update on #{comment.post.title}"
subject: default_i18n_subject(app_name: app_name, post: comment.post.title)
)
end
@@ -33,7 +35,7 @@ class UserMailer < ApplicationMailer
mail(
to: post.followers.pluck(:email),
subject: "[#{app_name}] Status change on post #{post.title}"
subject: default_i18n_subject(app_name: app_name, post: post.title)
)
end

View File

@@ -1,15 +1,21 @@
<%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %>
<h2>Resend confirmation instructions</h2>
<h2><%= t('common.forms.auth.resend_confirmation_instructions') %></h2>
<%= render "devise/shared/error_messages", resource: resource %>
<div class="form-group">
<%= f.label :email, class: "sr-only" %>
<%= f.email_field :email, autofocus: true, autocomplete: "email", placeholder: "Email address", required: true, value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email), class: "form-control" %>
<%= f.email_field :email,
autofocus: true,
autocomplete: "email",
placeholder: t('common.forms.auth.email'),
required: true,
value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email),
class: "form-control" %>
</div>
<div class="actions">
<%= f.submit "Resend confirmation instructions", class: "btn btn-dark btn-block" %>
<%= f.submit t('common.forms.auth.resend_confirmation_instructions'), class: "btn btn-dark btn-block" %>
</div>
<% end %>

View File

@@ -1,26 +1,35 @@
<h2>Change your password</h2>
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %>
<h2><%= t('common.forms.auth.change_password') %></h2>
<%= render "devise/shared/error_messages", resource: resource %>
<%= f.hidden_field :reset_password_token %>
<div class="form-group">
<%= f.label :password, "New password" %>
<%= f.password_field :password, autofocus: true, autocomplete: "new-password", class: "form-control" %>
<%= f.label :password, class: "sr-only" %>
<%= f.password_field :password,
autofocus: true,
autocomplete: "new-password",
placeholder: t('common.forms.auth.new_password'),
class: "form-control" %>
<% if @minimum_password_length %>
<small id="passwordHelp" class="form-text text-muted">
(<%= @minimum_password_length %> characters minimum)
(<%= t('common.forms.auth.password_help', { count: @minimum_password_length }) %>)
</small>
<% end %>
</div>
<div class="form-group">
<%= f.label :password_confirmation, "Confirm new password" %>
<%= f.password_field :password_confirmation, autocomplete: "new-password", class: "form-control" %>
<%= f.label :password_confirmation, class: "sr-only" %>
<%= f.password_field :password_confirmation,
autocomplete: "new-password",
placeholder: t('common.forms.auth.new_password_confirmation'),
class: "form-control" %>
</div>
<div class="actions">
<%= f.submit "Change my password", class: "btn btn-dark btn-primary" %>
<%= f.submit t('common.forms.auth.change_password'), class: "btn btn-dark btn-primary" %>
</div>
<% end %>

View File

@@ -1,15 +1,20 @@
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %>
<h2>Forgot your password?</h2>
<h2><%= t('common.forms.auth.forgot_password') %></h2>
<%= render "devise/shared/error_messages", resource: resource %>
<div class="form-group">
<%= f.label :email, class: "sr-only" %>
<%= f.email_field :email, autofocus: true, autocomplete: "email", placeholder: "Email address", required: true, class: "form-control" %>
<%= f.email_field :email,
autofocus: true,
autocomplete: "email",
placeholder: t('common.forms.auth.email'),
required: true,
class: "form-control" %>
</div>
<div class="actions">
<%= f.submit "Send me reset password instructions", class: "btn btn-dark btn-block" %>
<%= f.submit t('common.forms.auth.send_reset_password_instructions'), class: "btn btn-dark btn-block" %>
</div>
<% end %>

View File

@@ -1,26 +1,23 @@
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
<h2>Profile settings</h2>
<h2><%= t('common.forms.auth.profile_settings') %></h2>
<%= render "devise/shared/error_messages", resource: resource %>
<div class="form-group">
<%= f.label :full_name %>
<%= f.label t('common.forms.auth.full_name') %>
<%= f.text_field :full_name, autocomplete: "full-name", class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :email %>
<%= f.label t('common.forms.auth.email') %>
<%= f.email_field :email, autocomplete: "email", class: "form-control" %>
<small id="emailGravatarHelp" class="form-text text-muted">
the email is <a href="https://gravatar.com" target="_blank">gravatar</a>ized
</small>
</div>
<div class="form-group">
<%= f.label :notifications_enabled %>&nbsp;
<%= f.label t('common.forms.auth.notifications_enabled') %>&nbsp;
<%= f.check_box :notifications_enabled, style: "transform: scale(1.5)" %>
<small id="notificationsHelp" class="form-text text-muted">
if disabled, you won't receive any notification
<%= t('common.forms.auth.notifications_enabled_help') %>
</small>
</div>
@@ -29,25 +26,20 @@
<% end %>
<div class="form-group">
<%= f.label :password %>
<%= f.label t('common.forms.auth.password') %>
<%= f.password_field :password, autocomplete: "new-password", class: "form-control" %>
<% if @minimum_password_length %>
<small id="passwordEditHelp" class="form-text text-muted">
leave blank if you don't want to change it
</small>
<small id="passwordHelp" class="form-text text-muted">
<%= @minimum_password_length %> characters minimum
</small>
<% end %>
<small id="passwordEditHelp" class="form-text text-muted">
leave blank if you don't want to change your password
</small>
</div>
<div class="form-group">
<%= f.label :password_confirmation %>
<%= f.label t('common.forms.auth.password_confirmation') %>
<%= f.password_field :password_confirmation, autocomplete: "new-password", class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :current_password %>
<%= f.label t('common.forms.auth.current_password') %>
<%= f.password_field :current_password, autocomplete: "current-password", class: "form-control" %>
<small id="currentPasswordHelp" class="form-text text-muted">
we need your current password to confirm your changes
@@ -55,15 +47,18 @@
</div>
<div class="actions">
<%= f.submit "Update profile", class: "btn btn-dark btn-block" %>
<%= f.submit t('common.forms.auth.update_profile'), class: "btn btn-dark btn-block" %>
</div>
<% end %>
<br />
<div class="edit_user">
<h3>Cancel my account</h3>
<p>Unhappy?
<%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete, class: "btn btn-danger btn-block" %>
</p>
<h3><%= t('common.forms.auth.cancel_account') %></h3>
<%= button_to t('common.forms.auth.cancel_account'),
registration_path(resource_name),
data: { confirm: "Are you sure?" },
method: :delete,
class: "btn btn-danger btn-block" %>
</div>

View File

@@ -1,30 +1,44 @@
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
<h2>Sign up</h2>
<h2><%= t('common.forms.auth.sign_up') %></h2>
<%= render "devise/shared/error_messages", resource: resource %>
<div class="form-group">
<%= f.label :full_name, class: "sr-only" %>
<%= f.text_field :full_name, autofocus: true, placeholder: "Full name", required: true, class: "form-control" %>
<%= f.text_field :full_name,
autofocus: true,
placeholder: t('common.forms.auth.full_name'),
required: true,
class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :email, class: "sr-only" %>
<%= f.email_field :email, autocomplete: "email", placeholder: "Email address", required: true, class: "form-control" %>
<%= f.email_field :email,
autocomplete: "email",
placeholder: t('common.forms.auth.email'),
required: true,
class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :password, class: "sr-only" %>
<%= f.password_field :password, placeholder: "Password", required: true, class: "form-control" %>
<%= f.password_field :password,
placeholder: t('common.forms.auth.password'),
required: true,
class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :password_confirmation, class: "sr-only" %>
<%= f.password_field :password_confirmation, placeholder: "Password confirmation", required: true, class: "form-control" %>
<%= f.password_field :password_confirmation,
placeholder: t('common.forms.auth.password_confirmation'),
required: true,
class: "form-control" %>
</div>
<div class="actions">
<%= f.submit "Sign up", class: "btn btn-dark btn-block" %>
<%= f.submit t('common.forms.auth.sign_up'), class: "btn btn-dark btn-block" %>
</div>
<% end %>

View File

@@ -1,25 +1,33 @@
<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
<h2>Log in</h2>
<h2><%= t('common.forms.auth.log_in') %></h2>
<div class="form-group">
<%= f.label :email, class: "sr-only" %>
<%= f.email_field :email, autocomplete: "email", placeholder: "Email address", required: true, class: "form-control" %>
<%= f.email_field :email,
autocomplete: "email",
placeholder: t('common.forms.auth.email'),
required: true,
class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :password, class: "sr-only" %>
<%= f.password_field :password, autocomplete: "current-password", placeholder: "Password", required: true, class: "form-control" %>
<%= f.password_field :password,
autocomplete: "current-password",
placeholder: t('common.forms.auth.password'),
required: true,
class: "form-control" %>
</div>
<% if devise_mapping.rememberable? %>
<div class="form-group form-check">
<%= f.check_box :remember_me, class: "form-check-input" %>
<%= f.label :remember_me, class: "form-check-label" %>
<%= f.label t('common.forms.auth.remember_me'), class: "form-check-label" %>
</div>
<% end %>
<div class="actions">
<%= f.submit "Log in", class: "btn btn-dark btn-block" %>
<%= f.submit t('common.forms.auth.log_in'), class: "btn btn-dark btn-block" %>
</div>
<% end %>

View File

@@ -1,22 +1,22 @@
<div class="deviseLinks">
<%- if controller_name != 'sessions' %>
<%= link_to "Log in", new_session_path(resource_name) %>
<%= link_to t('common.forms.auth.log_in'), new_session_path(resource_name) %>
<% end %>
<%- if devise_mapping.registerable? && controller_name != 'registrations' %>
<%= link_to "Sign up", new_registration_path(resource_name) %>
<%= link_to t('common.forms.auth.sign_up'), new_registration_path(resource_name) %>
<% end %>
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
<%= link_to "Forgot your password?", new_password_path(resource_name) %>
<%= link_to t('common.forms.auth.forgot_password'), new_password_path(resource_name) %>
<% end %>
<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
<%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
<%= link_to t('common.forms.auth.confirmation_instructions_not_received'), new_confirmation_path(resource_name) %>
<% end %>
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
<%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
<%= link_to t('common.forms.auth.unlock_instructions_not_received'), new_unlock_path(resource_name) %>
<% end %>
<%- if devise_mapping.omniauthable? %>

View File

@@ -1,15 +1,18 @@
<h2>Resend unlock instructions</h2>
<%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %>
<h2><%= t('common.forms.auth.resend_unlock_instructions') %></h2>
<%= render "devise/shared/error_messages", resource: resource %>
<div class="field">
<%= f.label :email %><br />
<%= f.email_field :email, autofocus: true, autocomplete: "email" %>
<%= f.label :email, class: "sr-only" %><br />
<%= f.email_field :email,
autofocus: true,
autocomplete: "email"
placeholder: t('common.forms.auth.email') %>
</div>
<div class="actions">
<%= f.submit "Resend unlock instructions", class: "btn btn-dark btn-block" %>
<%= f.submit t('common.forms.auth.resend_unlock_instructions'), class: "btn btn-dark btn-block" %>
</div>
<% end %>

View File

@@ -26,18 +26,18 @@
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<% if current_user.power_user? %>
<%= link_to 'Site settings', site_settings_boards_path, class: 'dropdown-item' %>
<%= link_to 'Admin Panel', admin_root_path, class: 'dropdown-item', 'data-turbolinks': 'false' %>
<%= link_to t('header.menu.site_settings'), site_settings_boards_path, class: 'dropdown-item' %>
<%= link_to t('header.menu.admin_panel'), admin_root_path, class: 'dropdown-item', 'data-turbolinks': 'false' %>
<div class="dropdown-divider"></div>
<% end %>
<%= link_to 'Profile settings', edit_user_registration_path, class: 'dropdown-item' %>
<%= link_to t('header.menu.profile_settings'), edit_user_registration_path, class: 'dropdown-item' %>
<div class="dropdown-divider"></div>
<%= link_to 'Sign out', destroy_user_session_path, method: :delete, class: 'dropdown-item' %>
<%= link_to t('header.menu.sign_out'), destroy_user_session_path, method: :delete, class: 'dropdown-item' %>
</div>
</li>
<% else %>
<li class="nav-item">
<%= link_to 'Log in / Sign up', new_user_session_path, class: 'nav-link' %>
<%= link_to t('header.log_in'), new_user_session_path, class: 'nav-link' %>
</li>
<% end %>
</ul>

View File

@@ -0,0 +1,6 @@
<script type="text/javascript">
// Used to set I18n-js locale to Rails locale
// Variable used in javascript/translations/index.js.erb
var LOCALE = "<%= I18n.locale %>";
</script>

View File

@@ -8,6 +8,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="turbolinks-cache-control" content="no-cache">
<%= render 'layouts/set_js_locale' %>
<%= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>

View File

@@ -2,12 +2,8 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
/* Email styles need to be inline */
</style>
</head>
<body>
<%= yield %>
<div><%= yield %></div>
</body>
</html>

View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
<p><%= t('user_mailer.opening_greeting') %></p>
<div><%= yield %></div>
<p><%= t('user_mailer.closing_greeting') %></p>
<footer>
<%= link_to(t('user_mailer.unsubscribe'), edit_user_registration_url) %>.
</footer>
</body>
</html>

View File

@@ -0,0 +1 @@
<%= yield %>

View File

@@ -1,9 +1,9 @@
<div class="sidebar">
<div class="sidebarCard">
<span class="boxTitleText">Site Settings</span>
<span class="boxTitleText"><%= t('site_settings.menu.title') %></span>
<div class="verticalNavigation" role="tablist" aria-orientation="vertical">
<%= render 'menu_link', label: 'Boards', path: site_settings_boards_path %>
<%= render 'menu_link', label: 'Post statuses', path: site_settings_post_statuses_path %>
<%= render 'menu_link', label: t('site_settings.menu.boards'), path: site_settings_boards_path %>
<%= render 'menu_link', label: t('site_settings.menu.post_statuses'), path: site_settings_post_statuses_path %>
</div>
</div>
</div>

View File

@@ -1,4 +1,4 @@
<h2>Roadmap</h2>
<h2><%= t('roadmap.title') %></h2>
<%=
react_component(

View File

@@ -1,24 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta content='text/html; charset=utf-8' http-equiv='content-type' />
</head>
<body>
<h1>Hello, <%= @user.full_name %></h1>
<p>
There is a new reply by <b><%= @comment.user.full_name %></b> on your comment from post <b><%= @comment.post.title %></b>:
</p>
<p>
<i><%= @comment.body %></i>
</p>
<p>
<%= link_to "Click here", post_url(@comment.post) %> to have your say!
</p>
<p>
Have a great day!
</p>
</body>
<footer>
Annoyed? You can <%= link_to("turn off notifications here", edit_user_registration_url) %>.
</footer>
</html>
<p>
<%= t('user_mailer.notify_comment_owner.body', { user: @comment.user.full_name, post: @comment.post.title }) %>
</p>
<p>
<i><%= @comment.body %></i>
</p>
<p>
<%= link_to t('user_mailer.learn_more'), post_url(@comment.post) %>
</p>

View File

@@ -1,24 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta content='text/html; charset=utf-8' http-equiv='content-type' />
</head>
<body>
<h1>Hello!</h1>
<p>
The post you're following <b><%= @post.title %></b> has a new status:
<span style='background-color: <%= @post.post_status.color %>; color: white;'%>>
<%= @post.post_status.name %>
</span>
</p>
<p>
<%= link_to "Click here", post_url(@post) %> to learn more!
</p>
<p>
Have a great day!
</p>
</body>
<footer>
Annoyed? You can <%= link_to("turn off notifications here", edit_user_registration_url) %>.
</footer>
</html>
<p>
<%= I18n.t('user_mailer.notify_followers_of_post_status_change.body', { post: @post }) %>
<span style='background-color: <%= @post.post_status.color %>; color: white;'%>>
<%= @post.post_status.name %>
</span>
</p>
<p>
<%= link_to I18n.t('user_mailer.learn_more'), post_url(@post) %>
</p>

View File

@@ -1,24 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta content='text/html; charset=utf-8' http-equiv='content-type' />
</head>
<body>
<h1>Hello!</h1>
<p>
There is a new update on the post you're following <b><%= @comment.post.title %></b>:
</p>
<p>
<i><%= @comment.body %></i>
</p>
<p>
<%= link_to "Click here", post_url(@comment.post) %> to have your say!
</p>
<p>
Have a great day!
</p>
</body>
<footer>
Annoyed? You can <%= link_to("turn off notifications here", edit_user_registration_url) %>.
</footer>
</html>
<p>
<%= I18n.t('user_mailer.notify_followers_of_post_update.body', { user: @comment.user.full_name, post: @comment.post.title }) %>
</p>
<p>
<i><%= @comment.body %></i>
</p>
<p>
<%= link_to I18n.t('user_mailer.learn_more'), post_url(@comment.post) %>
</p>

View File

@@ -1,24 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta content='text/html; charset=utf-8' http-equiv='content-type' />
</head>
<body>
<h1>Hello, <%= @user.full_name %></h1>
<p>
There is a new comment by <b><%= @comment.user.full_name %></b> on your post <b><%= @comment.post.title %></b>:
</p>
<p>
<i><%= @comment.body %></i>
</p>
<p>
<%= link_to "Click here", post_url(@comment.post) %> to have your say!
</p>
<p>
Have a great day!
</p>
</body>
<footer>
Annoyed? You can <%= link_to("turn off notifications here", edit_user_registration_url) %>.
</footer>
</html>
<p>
<%= t('user_mailer.notify_post_owner.body', { user: @comment.user.full_name, post: @comment.post.title }) %>
</p>
<p>
<i><%= @comment.body %></i>
</p>
<p>
<%= link_to t('user_mailer.learn_more'), post_url(@comment.post) %>
</p>

View File

@@ -0,0 +1,5 @@
# Configure I18n to look at subfolders too
I18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.yml')]
I18n.available_locales = [:en, :it]
I18n.default_locale = :en

View File

@@ -0,0 +1,43 @@
it:
activerecord:
attributes:
board:
name: 'Nome'
description: 'Descrizione'
order: 'Ordine'
comment:
body: 'Corpo'
is_post_update: 'Aggiornamento post'
user_id: 'Autore'
post_id: 'Post'
parent_id: 'Commento padre'
follow:
user_id: 'Utente'
post_id: 'Post'
like:
user_id: 'Utente'
post_id: 'Post'
post_status:
name: 'Nome'
color: 'Colore'
order: 'Ordine'
show_in_roadmap: 'Mostra nella roadmap'
post:
title: 'Titolo'
description: 'Descrizione'
board_id: 'Bacheca del post'
user_id: 'Autore del post'
post_status_id: 'Stato del post'
user:
email: 'E-mail'
full_name: 'Nome e cognome'
role: 'Ruolo'
notifications_enabled: 'Notifiche abilitate'
errors:
messages:
invalid: 'è invalido'
required: 'è obbligatorio'
blank: 'non può essere vuoto'
taken: 'è già in uso'
too_short: 'è troppo corto (almeno %{count} caratteri)'
too_long: 'è troppo lungo (massimo ${count} caratteri)'

View File

@@ -0,0 +1,66 @@
# Italian translation for Devise 4.2
# Date: 2016-08-01
# Author: epistrephein, iwan
# Note: Thanks to xpepper (https://gist.github.com/xpepper/8052632)
# Additional translations at https://github.com/plataformatec/devise/wiki/I18n
it:
devise:
confirmations:
confirmed: "Il tuo account è stato correttamente confermato."
send_instructions: "Entro qualche minuto riceverai un messaggio email con le istruzioni per confermare il tuo account."
send_paranoid_instructions: "Se il tuo indirizzo email esiste nel nostro database, entro qualche minuto riceverai un messaggio email con le istruzioni per confermare il tuo account."
failure:
already_authenticated: "Hai già effettuato l'accesso."
inactive: "Il tuo account non è ancora stato attivato."
invalid: "%{authentication_keys} o password non validi."
locked: "Il tuo account è bloccato."
last_attempt: "Hai un altro tentativo prima che il tuo account venga bloccato."
not_found_in_database: "%{authentication_keys} o password non validi."
timeout: "La tua sessione è scaduta, accedi nuovamente per continuare."
unauthenticated: "Devi accedere o registrarti per continuare."
unconfirmed: "Devi confermare il tuo indirizzo email per continuare."
mailer:
confirmation_instructions:
subject: "Istruzioni per la conferma"
reset_password_instructions:
subject: "Istruzioni per reimpostare la password"
unlock_instructions:
subject: "Istruzioni per sbloccare l'account"
password_change:
subject: "Password reimpostata"
omniauth_callbacks:
failure: 'Non è stato possibile autenticarti come %{kind} perché "%{reason}".'
success: "Autenticato con successo dall'account %{kind}."
passwords:
no_token: "Non è possibile accedere a questa pagina se non provieni da una e-mail di ripristino della password. Se provieni da una e-mail di ripristino della password, assicurarti di utilizzare l'URL completo."
send_instructions: "Entro qualche minuto riceverai un messaggio email con le istruzioni per reimpostare la tua password."
send_paranoid_instructions: "Se il tuo indirizzo email esiste nel nostro database, entro qualche minuto riceverai un messaggio email con le istruzioni per ripristinare la password."
updated: "La tua password è stata cambiata correttamente. Ora sei collegato."
updated_not_active: "La tua password è stata cambiata correttamente."
registrations:
destroyed: "Arrivederci! Il tuo account è stato cancellato. Speriamo di rivederti presto."
signed_up: "Benvenuto! Ti sei registrato correttamente."
signed_up_but_inactive: "Ti sei registrato correttamente. Tuttavia non puoi effettuare l'accesso perché il tuo account non è stato ancora attivato."
signed_up_but_locked: "Ti sei registrato correttamente. Tuttavia non puoi effettuare l'accesso perché il tuo account è bloccato."
signed_up_but_unconfirmed: "Ti sei registrato correttamente. Un messaggio con il link per confermare il tuo account è stato inviato al tuo indirizzo email."
update_needs_confirmation: "Il tuo account è stato aggiornato, tuttavia è necessario verificare il tuo nuovo indirizzo email. Entro qualche minuto riceverai un messaggio email con le istruzioni per confermare il tuo nuovo indirizzo email."
updated: "Il tuo account è stato aggiornato."
sessions:
signed_in: "Accesso effettuato con successo."
signed_out: "Sei uscito correttamente."
already_signed_out: "Sei uscito correttamente."
unlocks:
send_instructions: "Entro qualche minuto riceverai un messaggio email con le istruzioni per sbloccare il tuo account."
send_paranoid_instructions: "Se il tuo indirizzo email esiste nel nostro database, entro qualche minuto riceverai un messaggio email con le istruzioni per sbloccare il tuo account."
unlocked: "Il tuo account è stato correttamente sbloccato. Accedi per continuare."
errors:
messages:
already_confirmed: "è stato già confermato, prova ad effettuare un nuovo accesso"
confirmation_period_expired: "deve essere confermato entro %{period}, si prega di richiederne uno nuovo"
expired: "è scaduto, si prega di richiederne uno nuovo"
not_found: "non trovato"
not_locked: "non era bloccato"
not_saved:
one: "Un errore ha impedito di salvare questo %{resource}:"
other: "%{count} errori hanno impedito di salvare questo %{resource}:"

View File

@@ -1,11 +1,148 @@
en:
errors:
unauthorized: 'You are not authorized'
post:
create: 'Post create error: %{message}'
update: 'Post update error: %{message}'
like:
create: 'Like create error: %{message}'
comment:
create: 'Comment create error: %{message}'
update: 'Comment update error: %{message}'
common:
forms:
auth:
email: 'Email address'
full_name: 'Full name'
password: 'Password'
password_confirmation: 'Password confirmation'
new_password: 'New password'
new_password_confirmation: 'New password confirmation'
current_password: 'Current password'
notifications_enabled: 'Notifications enabled'
notifications_enabled_help: "if disabled, you won't receive any notification"
remember_me: 'Remember me'
log_in: 'Log in'
sign_up: 'Sign up'
profile_settings: 'Profile settings'
update_profile: 'Update profile'
cancel_account: 'Cancel account'
forgot_password: 'Forgot your password?'
confirmation_instructions_not_received: "Didn't receive confirmation instructions?"
unlock_instructions_not_received: "Didn't receive unlock instructions?"
send_reset_password_instructions: 'Send me reset password instructions'
resend_confirmation_instructions: 'Resend confirmation instructions'
resend_unlock_instructions: 'Resend unlock instructions'
change_password: 'Change password'
password_help: '%{count} characters minimum'
comments_number:
one: '1 comment'
other: '%{count} comments'
no_status: 'No status'
loading: 'Loading...'
buttons:
edit: 'Edit'
delete: 'Delete'
cancel: 'Cancel'
create: 'Create'
update: 'Save'
datetime:
now: 'just now'
minutes:
one: '1 minute ago'
other: '%{count} minutes ago'
hours:
one: '1 hour ago'
other: '%{count} hours ago'
days:
one: '1 day ago'
other: '%{count} days ago'
header:
menu:
site_settings: 'Site settings'
admin_panel: 'Admin panel (deprecated)'
profile_settings: 'Profile settings'
sign_out: 'Sign out'
log_in: 'Log in / Sign up'
roadmap:
title: 'Roadmap'
board:
new_post:
submit_button: 'Submit feedback'
cancel_button: 'Cancel'
login_button: 'Log in / Sign up'
title: 'Title'
description: 'Description (optional)'
no_title: 'Title field is mandatory'
submit_success: 'Feedback published! You will be redirected soon...'
submit_error: 'An unknown error occurred, try again'
search_box:
title: 'Search'
filter_box:
title: 'Filter by status'
posts_list:
empty: 'There are no posts'
post:
edit_button: 'Edit'
post_status_select:
no_post_status: 'None'
updates_box:
title: 'Updates'
empty: 'There are no updates yet'
status_change: 'changed status to'
likes_box:
title: 'Likes'
empty: 'There are no likes yet'
action_box:
title: 'Actions'
follow_button: 'Follow'
unfollow_button: 'Unfollow'
following_description: "you're receiving notifications about new updates on this post"
not_following_description: "you won't receive notifications about this post"
comments:
title: 'Activity'
post_update_badge: 'Update'
reply_button: 'Reply'
new_comment:
body_placeholder: 'Leave a comment'
submit_button: 'Submit'
is_post_update: 'Mark as update'
user_notification: 'Users that follow this post will be notified'
not_logged_in: 'You need to log in to post comments'
site_settings:
menu:
title: 'Site settings'
boards: 'Boards'
post_statuses: 'Statuses'
info_box:
up_to_date: 'All changes saved'
error: 'An error occurred: %{message}'
boards:
title: 'Boards'
empty: 'There are no boards. Create one below!'
new: 'New'
form:
name: 'Board name'
description: 'Board description (optional)'
post_statuses:
title: 'Statuses'
empty: 'There are no statuses. Create one below!'
new: 'New'
form:
name: 'Status name'
user_mailer:
opening_greeting: 'Hello!'
closing_greeting: 'Have a great day!'
learn_more: 'Click here to learn more'
unsubscribe: 'Annoyed? You can turn off notifications here'
notify_post_owner:
subject: '[%{app_name}] New comment on %{post}'
body: 'There is a new comment by %{user} on your post %{post}'
notify_comment_owner:
subject: '[%{app_name}] New reply on your comment from %{post}'
body: 'There is a new reply by %{user} on your comment from post %{post}'
notify_followers_of_post_update:
subject: '[%{app_name}] New update for post %{post}'
body: "There is a new update on the post you're following %{post}"
notify_followers_of_post_status_change:
subject: '[%{app_name}] Status change on post %{post}'
body: "The post you're following %{post} has a new status"
backend:
errors:
unauthorized: 'You are not authorized'
not_logged_in: 'You must be logged in to access this page'
not_enough_privileges: 'You do not have the privilegies to access this page'
board:
update_order: 'There was an error in reordering boards'
post_status:
update_order: 'There was an error in reordering statuses'

148
config/locales/it.yml Normal file
View File

@@ -0,0 +1,148 @@
it:
common:
forms:
auth:
email: 'Indirizzo email'
full_name: 'Nome e cognome'
password: 'Password'
password_confirmation: 'Conferma password'
new_password: 'Nuova password'
new_password_confirmation: 'Conferma nuova password'
current_password: 'Password corrente'
notifications_enabled: 'Notifiche abilitate'
notifications_enabled_help: "se disabilitato, non riceverai alcuna notifica"
remember_me: 'Ricordami'
log_in: 'Accedi'
sign_up: 'Registrati'
profile_settings: 'Impostazioni profilo'
update_profile: 'Aggiorna profilo'
cancel_account: 'Cancella account'
forgot_password: 'Password dimenticata?'
confirmation_instructions_not_received: 'Non hai ricevuto le istruzioni di conferma?'
unlock_instructions_not_received: 'Non hai ricevuto le istruzioni di sblocco?'
send_reset_password_instructions: 'Invia istruzioni per reset password'
resend_confirmation_instructions: 'Invia istruzioni di conferma'
resend_unlock_instructions: 'Invia istruzioni di sblocco'
change_password: 'Cambia password'
password_help: '%{count} caratteri minimo'
comments_number:
one: '1 commento'
other: '%{count} commenti'
no_status: 'Nessuno stato'
loading: 'Caricamento...'
buttons:
edit: 'Modifica'
delete: 'Elimina'
cancel: 'Annulla'
create: 'Crea'
update: 'Salva'
datetime:
now: 'adesso'
minutes:
one: '1 minuto fa'
other: '%{count} minuti fa'
hours:
one: '1 ora fa'
other: '%{count} ore fa'
days:
one: '1 giorno fa'
other: '%{count} giorni fa'
header:
menu:
site_settings: 'Impostazioni sito'
admin_panel: 'Admin panel (deprecato)'
profile_settings: 'Impostazioni profilo'
sign_out: 'Esci'
log_in: 'Accedi / Registrati'
roadmap:
title: 'Roadmap'
board:
new_post:
submit_button: 'Invia feedback'
cancel_button: 'Annulla'
login_button: 'Accedi / Registrati'
title: 'Titolo'
description: 'Descrizione (opzionale)'
no_title: 'Il campo titolo è obbligatorio'
submit_success: 'Feedback pubblicato! Sarai reindirizzato a breve...'
submit_error: 'Si è verificato un errore sconosciuto, riprova'
search_box:
title: 'Cerca'
filter_box:
title: 'Filtra per stato'
posts_list:
empty: 'Non ci sono post'
post:
edit_button: 'Modifica'
post_status_select:
no_post_status: 'Nessuno'
updates_box:
title: 'Aggiornamenti'
empty: 'Non ci sono aggiornamenti per ora'
status_change: 'stato cambiato in'
likes_box:
title: 'Mi piace'
empty: 'Non ci sono mi piace per ora'
action_box:
title: 'Azioni'
follow_button: 'Segui'
unfollow_button: 'Non seguire più'
following_description: 'riceverai notifiche sugli aggiornamenti di questo post'
not_following_description: 'non riceverai alcuna notifica riguardo questo post'
comments:
title: 'Attività'
post_update_badge: 'Aggiornamento'
reply_button: 'Rispondi'
new_comment:
body_placeholder: 'Lascia un commento'
submit_button: 'Invia'
is_post_update: 'Contrassegna come aggiornamento'
user_notification: 'Gli utenti che seguono questo post saranno notificati'
not_logged_in: 'Devi effettuare il log in per commentare'
site_settings:
menu:
title: 'Impostazioni sito'
boards: 'Bacheche'
post_statuses: 'Stati'
info_box:
up_to_date: 'Tutte le modifiche sono state salvate'
error: 'Si è verificato un errore: %{message}'
boards:
title: 'Bacheche'
empty: 'Non ci sono bacheche. Creane una qua sotto!'
new: 'Nuova'
form:
name: 'Nome bacheca'
description: 'Descrizione bacheca (opzionale)'
post_statuses:
title: 'Stati'
empty: 'Non ci sono stati. Creane uno qua sotto!'
new: 'Nuovo'
form:
name: 'Nome stato'
user_mailer:
opening_greeting: 'Ciao!'
closing_greeting: 'Buona giornata!'
learn_more: 'Clicca qui per saperne di più'
unsubscribe: 'Non vuoi più ricevere notifiche? Clicca qui'
notify_post_owner:
subject: '[%{app_name}] Nuovo commento al tuo post %{post}'
body: '%{user} ha commentato il tuo post %{post}'
notify_comment_owner:
subject: '[%{app_name}] Risposta al tuo commento nel post %{post}'
body: '%{user} ha risposto al tuo commento nel post %{post}'
notify_followers_of_post_update:
subject: '[%{app_name}] Nuovo aggiornamento per il post %{post}'
body: "There is a new update on the post you're following %{post}"
notify_followers_of_post_status_change:
subject: '[%{app_name}] Aggiornamento stato per il post %{post}'
body: "Il post che segui %{post} ha un nuovo stato"
backend:
errors:
unauthorized: 'Non sei autorizzato'
not_logged_in: "Devi effettuare l'accesso per visualizzare questa pagina"
not_enough_privileges: 'Non hai i privilegi necessari per visualizzare questa pagina'
board:
update_order: 'Si è verificato un errore durante il riordinamento delle bacheche'
post_status:
update_order: 'Si è verificato un errore durante il riordinamento degli stati'

View File

@@ -1,4 +1,5 @@
const { environment } = require('@rails/webpacker')
const erb = require('./loaders/erb')
const typescript = require('./loaders/typescript')
const webpack = require('webpack')
@@ -12,4 +13,5 @@ environment.plugins.append(
})
)
environment.loaders.prepend('erb', erb)
module.exports = environment

View File

@@ -0,0 +1,11 @@
module.exports = {
test: /\.erb$/,
enforce: 'pre',
exclude: /node_modules/,
use: [{
loader: 'rails-erb-loader',
options: {
runner: (/^win/.test(process.platform) ? 'ruby ' : '') + 'bin/rails runner'
}
}]
}

View File

@@ -34,6 +34,7 @@ default: &default
- .woff2
extensions:
- .erb
- .tsx
- .ts
- .jsx

View File

@@ -11,9 +11,11 @@
"@types/react-dom": "^16.9.0",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"bootstrap": "4.3.1",
"i18n-js": "^3.9.2",
"jquery": "^3.5.1",
"popper.js": "^1.15.0",
"prop-types": "^15.7.2",
"rails-erb-loader": "^5.5.2",
"react": "^16.9.0",
"react-beautiful-dnd": "^13.1.0",
"react-dom": "^16.9.0",

View File

@@ -8,7 +8,6 @@ RSpec.describe UserMailer, type: :mailer do
let(:mail) { UserMailer.notify_post_owner(comment: comment) }
it "renders the headers" do
expect(mail.subject).to eq("[#{ENV.fetch('APP_NAME')}] New comment on #{post.title}")
expect(mail.to).to eq(["notified@example.com"])
expect(mail.from).to eq(["notifications@example.com"])
end

View File

@@ -3474,6 +3474,11 @@ https-browserify@^1.0.0:
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
i18n-js@^3.9.2:
version "3.9.2"
resolved "https://registry.yarnpkg.com/i18n-js/-/i18n-js-3.9.2.tgz#4a015dcfabd4c9fc73115fc2d02d2627e4c15ca5"
integrity sha512-+Gm8h5HL0emzKhRx2avMKX+nKiVPXeaOZm7Euf2/pbbFcLQoJ3zZYiUykAzoRasijCoWos2Kl1tslmScTgAQKw==
iconv-lite@0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@@ -4124,6 +4129,11 @@ lodash._reinterpolate@^3.0.0:
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=
lodash.defaults@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=
lodash.get@^4.0:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
@@ -5982,6 +5992,14 @@ raf-schd@^4.0.2:
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a"
integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==
rails-erb-loader@^5.5.2:
version "5.5.2"
resolved "https://registry.yarnpkg.com/rails-erb-loader/-/rails-erb-loader-5.5.2.tgz#db3fa8ac89600f09d179a1a70a2ca18c592576ea"
integrity sha512-cjQH9SuSvRPhnWkvjmmAW/S4AFVDfAtYnQO4XpKJ8xpRdZayT73iXoE+IPc3VzN03noZXhVmyvsCvKvHj4LY6w==
dependencies:
loader-utils "^1.1.0"
lodash.defaults "^4.2.0"
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"