Merge branch 'master' of github.com:umputun/remark into small-improvements
This commit is contained in:
@@ -29,33 +29,35 @@ jobs:
|
||||
- name: install go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.14
|
||||
|
||||
- name: test backend
|
||||
run: |
|
||||
date
|
||||
cd backend/app
|
||||
go test -mod=vendor -timeout=60s -covermode=count -coverprofile=$GITHUB_WORKSPACE/profile.cov_tmp ./...
|
||||
cat $GITHUB_WORKSPACE/profile.cov_tmp | grep -v "_mock.go" > $GITHUB_WORKSPACE/profile.cov
|
||||
cd ../_example/memory_store
|
||||
go test -race ./...
|
||||
env:
|
||||
TZ: "America/Chicago"
|
||||
go-version: 1.13
|
||||
|
||||
- name: install golangci-lint and goveralls
|
||||
run: |
|
||||
curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $GITHUB_WORKSPACE v1.20.0
|
||||
curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $GITHUB_WORKSPACE v1.23.0
|
||||
go get -u github.com/mattn/goveralls
|
||||
|
||||
- name: run backend linters
|
||||
run: $GITHUB_WORKSPACE/golangci-lint run --config .golangci.yml ./...
|
||||
working-directory: backend
|
||||
- name: test and lint backend
|
||||
run: |
|
||||
go test -timeout=60s -covermode=count -coverprofile=$GITHUB_WORKSPACE/profile.cov_tmp ./...
|
||||
cat $GITHUB_WORKSPACE/profile.cov_tmp | grep -v "_mock.go" > $GITHUB_WORKSPACE/profile.cov
|
||||
$GITHUB_WORKSPACE/golangci-lint --config ${GITHUB_WORKSPACE}/backend/.golangci.yml run ./...
|
||||
working-directory: backend/app
|
||||
env:
|
||||
GOFLAGS: "-mod=vendor"
|
||||
TZ: "America/Chicago"
|
||||
|
||||
- name: run linters for examples
|
||||
run: $GITHUB_WORKSPACE/golangci-lint run --config ${GITHUB_WORKSPACE}/backend/.golangci.yml ./...
|
||||
- name: test and lint examples
|
||||
run: |
|
||||
go version
|
||||
$GITHUB_WORKSPACE/golangci-lint version
|
||||
go test -race ./...
|
||||
$GITHUB_WORKSPACE/golangci-lint --config ${GITHUB_WORKSPACE}/backend/.golangci.yml run ./...
|
||||
working-directory: backend/_example/memory_store
|
||||
env:
|
||||
TZ: "America/Chicago"
|
||||
|
||||
- name: submit coverage
|
||||
uses: shogo82148/actions-goveralls@v1
|
||||
with:
|
||||
path-to-profile: ${{ github.workspace }}/profile.cov
|
||||
working-directory: backend
|
||||
run: $(go env GOPATH)/bin/goveralls -service="github" -coverprofile=$GITHUB_WORKSPACE/profile.cov
|
||||
working-directory: backend
|
||||
env:
|
||||
COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Umputun
|
||||
Copyright (c) 2020 Umputun
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -155,6 +155,7 @@ _this is the recommended way to run remark42_
|
||||
| notify.telegram.timeout | NOTIFY_TELEGRAM_TIMEOUT | `5s` | telegram timeout |
|
||||
| notify.email.fromAddress | NOTIFY_EMAIL_FROM | | from email address |
|
||||
| notify.email.verification_subj | NOTIFY_EMAIL_VERIFICATION_SUBJ | `Email verification` | verification message subject |
|
||||
| notify.email.notify_admin | NOTIFY_EMAIL_ADMIN | `false` | notify admin on new comments via ADMIN_SHARED_EMAIL |
|
||||
| smtp.host | SMTP_HOST | | SMTP host |
|
||||
| smtp.port | SMTP_PORT | | SMTP port |
|
||||
| smtp.username | SMTP_USERNAME | | SMTP user name |
|
||||
@@ -472,9 +473,10 @@ window.REMARK42.changeTheme('light');
|
||||
|
||||
##### Locales
|
||||
|
||||
Right now Remark has support three locales en, ru (partial translated), de and fi.
|
||||
You can pick one using configuration object.
|
||||
Do you want support other locale? Please create [issue](https://github.com/umputun/remark42/issues).
|
||||
Right now Remark is translated to en, ru (partially), de, and fi languages.
|
||||
You can pick one using [configuration object](#setup-on-your-website).
|
||||
|
||||
Do you want translate remark42 to other locale? Please see [this documentation](https://github.com/umputun/remark42/blob/master/docs/translation.md) for details.
|
||||
|
||||
#### Last comments
|
||||
|
||||
@@ -587,8 +589,8 @@ It stars backend service with embedded bolt store on port `8080` with basic auth
|
||||
|
||||
#### Build
|
||||
|
||||
* install [Node.js 8](https://nodejs.org/en/) or higher;
|
||||
* install [NPM 6.1.0](https://www.npmjs.com/package/npm);
|
||||
* install [Node.js 12.11](https://nodejs.org/en/) or higher;
|
||||
* install [NPM 6.13.4](https://www.npmjs.com/package/npm);
|
||||
* run `npm install` inside `./frontend`;
|
||||
* run `npm run build` there;
|
||||
* result files will be saved in `./frontend/public`.
|
||||
|
||||
@@ -200,6 +200,7 @@ type NotifyGroup struct {
|
||||
Email struct {
|
||||
From string `long:"fromAddress" env:"FROM" description:"from email address"`
|
||||
VerificationSubject string `long:"verification_subj" env:"VERIFICATION_SUBJ" description:"verification message subject"`
|
||||
AdminNotifications bool `long:"notify_admin" env:"ADMIN" description:"notify admin on new comments via ADMIN_SHARED_EMAIL"`
|
||||
} `group:"email" namespace:"email" env-namespace:"EMAIL"`
|
||||
}
|
||||
|
||||
@@ -437,6 +438,11 @@ func (s *ServerCommand) newServerApp() (*serverApp, error) {
|
||||
SimpleView: s.SimpleView,
|
||||
}
|
||||
|
||||
// enable admin notifications only if admin email is set
|
||||
if s.Notify.Email.AdminNotifications && s.Admin.Shared.Email != "" {
|
||||
srv.AdminEmail = s.Admin.Shared.Email
|
||||
}
|
||||
|
||||
srv.ScoreThresholds.Low, srv.ScoreThresholds.Critical = s.LowScore, s.CriticalScore
|
||||
|
||||
var devAuth *provider.DevAuthServer
|
||||
|
||||
+47
-26
@@ -88,6 +88,7 @@ type msgTmplData struct {
|
||||
PostTitle string
|
||||
Email string
|
||||
UnsubscribeLink string
|
||||
ForAdmin bool
|
||||
}
|
||||
|
||||
// verifyTmplData store data for verification message template execution
|
||||
@@ -133,8 +134,13 @@ const (
|
||||
<body>
|
||||
<div style="font-family: Helvetica, Arial, sans-serif; font-size: 18px; width: 100%; max-width: 640px; margin: auto;">
|
||||
<h1 style="text-align: center; position: relative; color: #4fbbd6; margin-top: 10px; margin-bottom: 10px;">Remark42</h1>
|
||||
{{- if .ForAdmin}}
|
||||
<div style="font-size: 16px; text-align: center; margin-bottom: 10px; color:#000!important;">New comment from {{.UserName}} on your site {{if .PostTitle}} to «{{.PostTitle}}»{{ end }}</div>
|
||||
{{- else }}
|
||||
<div style="font-size: 16px; text-align: center; margin-bottom: 10px; color:#000!important;">New reply from {{.UserName}} on your comment{{if .PostTitle}} to «{{.PostTitle}}»{{ end }}</div>
|
||||
{{- end }}
|
||||
<div style="background-color: #eee; padding: 15px 20px 20px 20px; border-radius: 3px;">
|
||||
{{- if .ParentCommentText}}
|
||||
<div style="margin-bottom: 12px; line-height: 24px; word-break: break-all;">
|
||||
<img src="{{.ParentUserPicture}}" style="width: 24px; height: 24px; display: inline; vertical-align: middle; margin: 0 8px 0 0; border-radius: 3px; background-color: #ccc;"/>
|
||||
<span style="font-size: 14px; font-weight: bold; color: #777">{{.ParentUserName}}</span>
|
||||
@@ -144,6 +150,7 @@ const (
|
||||
<div style="font-size: 14px; color:#333!important; padding: 0 14px 0 2px; border-radius: 3px; line-height: 1.4;">
|
||||
{{.ParentCommentText}}
|
||||
</div>
|
||||
{{- end }}
|
||||
<div style="padding-left: 20px; border-left: 1px dotted rgba(0,0,0,0.15); margin-top: 15px; padding-top: 5px;">
|
||||
<div style="margin-bottom: 12px;" line-height: 24px;word-break: break-all;>
|
||||
<img src="{{.UserPicture}}" style="width: 24px; height: 24px; display:inline; vertical-align:middle; margin: 0 8px 0 0; border-radius: 3px; background-color: #ccc;"/>
|
||||
@@ -155,9 +162,11 @@ const (
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align: center; font-size: 14px; margin-top: 32px;">
|
||||
<i style="color: #000!important;">Sent to <a style="color:inherit; text-decoration: none" href="mailto:{{.Email}}">{{.Email}}</a> for {{.ParentUserName}}</i>
|
||||
<i style="color: #000!important;">Sent to <a style="color:inherit; text-decoration: none" href="mailto:{{.Email}}">{{.Email}}</a>{{if not .ForAdmin}} for {{.ParentUserName}}{{ end }}</i>
|
||||
<div style="margin: auto; width: 150px; border-top: 1px solid rgba(0, 0, 0, 0.15); padding-top: 15px; margin-top: 15px;"></div>
|
||||
{{- if .UnsubscribeLink}}
|
||||
<a style="color: #0aa;" href="{{.UnsubscribeLink}}">Unsubscribe</a>
|
||||
{{- end }}
|
||||
<!-- This is hack for remove collapser in Gmail which can collapse end of the message -->
|
||||
<div style="opacity: 0;font-size: 1;">[{{.CommentDate.Format "02.01.2006 at 15:04"}}]</div>
|
||||
</div>
|
||||
@@ -176,10 +185,10 @@ const (
|
||||
<div style="text-align: center; font-family: Helvetica, Arial, sans-serif; font-size: 18px;">
|
||||
<h1 style="position: relative; color: #4fbbd6; margin-top: 0.2em;">Remark42</h1>
|
||||
<p style="position: relative; max-width: 20em; margin: 0 auto 1em auto; line-height: 1.4em; color:#000!important;">Confirmation for <b>{{.User}}</b> on site <b>{{.Site}}</b></p>
|
||||
{{if .SubscribeURL}}
|
||||
{{- if .SubscribeURL}}
|
||||
<p style="position: relative; margin: 0 0 0.5em 0;color:#000!important;"><a href="{{.SubscribeURL}}{{.Token}}">Click here to subscribe to email notifications</a></p>
|
||||
<p style="position: relative; margin: 0 0 0.5em 0;color:#000!important;">Alternatively, you can use code below for subscription.</p>
|
||||
{{ end }}
|
||||
{{- end }}
|
||||
<div style="background-color: #eee; max-width: 20em; margin: 0 auto; border-radius: 0.4em; padding: 0.5em;">
|
||||
<p style="position: relative; margin: 0 0 0.5em 0;color:#000!important;">TOKEN</p>
|
||||
<p style="position: relative; font-size: 0.7em; opacity: 0.8;"><i style="color:#000!important;">Copy and paste this text into “token” field on comments page</i></p>
|
||||
@@ -227,10 +236,9 @@ func NewEmail(emailParams EmailParams, smtpParams SmtpParams) (*Email, error) {
|
||||
return &res, err
|
||||
}
|
||||
|
||||
// Send email about reply to Request.Email if it's set, otherwise do nothing and return nil, thread safe
|
||||
// do not returns sending error, only following:
|
||||
// 1. (likely impossible) template execution error from email message creation from Request
|
||||
// 2. message dropped without sending in case of closed ctx
|
||||
// Send email about comment reply to Request.Email if it's set,
|
||||
// also sends email to site administrator if appropriate option is set.
|
||||
// Thread safe
|
||||
func (e *Email) Send(ctx context.Context, req Request) (err error) {
|
||||
if req.Email == "" {
|
||||
// this means we can't send this request via Email
|
||||
@@ -252,12 +260,12 @@ func (e *Email) Send(ctx context.Context, req Request) (err error) {
|
||||
}
|
||||
|
||||
if req.Comment.ID != "" {
|
||||
if req.parent.User.ID == req.Comment.User.ID {
|
||||
if req.parent.User.ID == req.Comment.User.ID && !req.ForAdmin {
|
||||
// don't send anything if if user replied to their own comment
|
||||
return nil
|
||||
}
|
||||
log.Printf("[DEBUG] send notification via %s, comment id %s", e, req.Comment.ID)
|
||||
msg, err = e.buildMessageFromRequest(req)
|
||||
msg, err = e.buildMessageFromRequest(req, req.ForAdmin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -288,33 +296,46 @@ func (e *Email) buildVerificationMessage(user, email, token, site string) (strin
|
||||
}
|
||||
|
||||
// buildMessageFromRequest generates email message based on Request using e.MsgTemplate
|
||||
func (e *Email) buildMessageFromRequest(req Request) (string, error) {
|
||||
func (e *Email) buildMessageFromRequest(req Request, forAdmin bool) (string, error) {
|
||||
subject := "New reply to your comment"
|
||||
if forAdmin {
|
||||
subject = "New comment to your site"
|
||||
}
|
||||
if req.Comment.PostTitle != "" {
|
||||
subject += fmt.Sprintf(" for \"%s\"", req.Comment.PostTitle)
|
||||
}
|
||||
|
||||
token, err := e.TokenGenFn(req.parent.User.ID, req.Email, req.Comment.Locator.SiteID)
|
||||
unsubscribeLink := e.UnsubscribeURL + "?site=" + req.Comment.Locator.SiteID + "&tkn=" + token
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "error creating token for unsubscribe link")
|
||||
}
|
||||
unsubscribeLink := e.UnsubscribeURL + "?site=" + req.Comment.Locator.SiteID + "&tkn=" + token
|
||||
if forAdmin {
|
||||
unsubscribeLink = ""
|
||||
}
|
||||
|
||||
commentUrlPrefix := req.Comment.Locator.URL + uiNav
|
||||
msg := bytes.Buffer{}
|
||||
err = e.msgTmpl.Execute(&msg, msgTmplData{
|
||||
UserName: req.Comment.User.Name,
|
||||
UserPicture: req.Comment.User.Picture,
|
||||
CommentText: req.Comment.Text,
|
||||
CommentLink: commentUrlPrefix + req.Comment.ID,
|
||||
CommentDate: req.Comment.Timestamp,
|
||||
ParentUserName: req.parent.User.Name,
|
||||
ParentUserPicture: req.parent.User.Picture,
|
||||
ParentCommentText: req.parent.Text,
|
||||
ParentCommentLink: commentUrlPrefix + req.parent.ID,
|
||||
ParentCommentDate: req.parent.Timestamp,
|
||||
PostTitle: req.Comment.PostTitle,
|
||||
Email: req.Email,
|
||||
UnsubscribeLink: unsubscribeLink,
|
||||
})
|
||||
tmplData := msgTmplData{
|
||||
UserName: req.Comment.User.Name,
|
||||
UserPicture: req.Comment.User.Picture,
|
||||
CommentText: req.Comment.Text,
|
||||
CommentLink: commentUrlPrefix + req.Comment.ID,
|
||||
CommentDate: req.Comment.Timestamp,
|
||||
PostTitle: req.Comment.PostTitle,
|
||||
Email: req.Email,
|
||||
UnsubscribeLink: unsubscribeLink,
|
||||
ForAdmin: forAdmin,
|
||||
}
|
||||
// in case of message to admin, parent message might be empty
|
||||
if req.Comment.ParentID != "" {
|
||||
tmplData.ParentUserName = req.parent.User.Name
|
||||
tmplData.ParentUserPicture = req.parent.User.Picture
|
||||
tmplData.ParentCommentText = req.parent.Text
|
||||
tmplData.ParentCommentLink = commentUrlPrefix + req.parent.ID
|
||||
tmplData.ParentCommentDate = req.parent.Timestamp
|
||||
}
|
||||
err = e.msgTmpl.Execute(&msg, tmplData)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "error executing template to build comment reply message")
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@ func TestEmail_Send(t *testing.T) {
|
||||
email.TokenGenFn = TokenGenFn
|
||||
email.UnsubscribeURL = "https://remark42.com/api/v1/email/unsubscribe"
|
||||
req := Request{
|
||||
Comment: store.Comment{ID: "999", User: store.User{ID: "1", Name: "test_user"}, PostTitle: "test_title"},
|
||||
Comment: store.Comment{ID: "999", User: store.User{ID: "1", Name: "test_user"}, ParentID: "1", PostTitle: "test_title"},
|
||||
parent: store.Comment{ID: "1", User: store.User{ID: "999", Name: "parent_user"}},
|
||||
Email: "test@example.org",
|
||||
}
|
||||
@@ -196,7 +196,7 @@ func TestEmail_Send(t *testing.T) {
|
||||
assert.Equal(t, 1, fakeSmtp.readQuitCount())
|
||||
assert.Equal(t, "test@example.org", fakeSmtp.readRcpt())
|
||||
// test buildMessageFromRequest separately for message text
|
||||
res, err := email.buildMessageFromRequest(req)
|
||||
res, err := email.buildMessageFromRequest(req, req.ForAdmin)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, res, `From: from@example.org
|
||||
To: test@example.org
|
||||
@@ -206,6 +206,23 @@ MIME-version: 1.0
|
||||
Content-Type: text/html; charset="UTF-8"
|
||||
List-Unsubscribe-Post: List-Unsubscribe=One-Click
|
||||
List-Unsubscribe: <https://remark42.com/api/v1/email/unsubscribe?site=&tkn=token>
|
||||
Date: `)
|
||||
|
||||
// send email to admin without parent set
|
||||
req = Request{
|
||||
Comment: store.Comment{ID: "999", User: store.User{ID: "1", Name: "test_user"}, PostTitle: "test_title"},
|
||||
Email: "admin@example.org",
|
||||
ForAdmin: true,
|
||||
}
|
||||
assert.NoError(t, email.Send(context.TODO(), req))
|
||||
res, err = email.buildMessageFromRequest(req, req.ForAdmin)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, res, `From: from@example.org
|
||||
To: admin@example.org
|
||||
Subject: New comment to your site for "test_title"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
MIME-version: 1.0
|
||||
Content-Type: text/html; charset="UTF-8"
|
||||
Date: `)
|
||||
}
|
||||
|
||||
|
||||
@@ -37,9 +37,11 @@ type Store interface {
|
||||
|
||||
// Request notification either about comment or about particular user verification
|
||||
type Request struct {
|
||||
Comment store.Comment // if set sent notifications about new comment
|
||||
parent store.Comment // fetched only in case Comment is set
|
||||
Email string // if set (also) send email
|
||||
Comment store.Comment // if set sent notifications about new comment
|
||||
parent store.Comment // fetched only in case Comment is set
|
||||
Email string // if set (also) send email
|
||||
ForAdmin bool // if set, message supposed to be sent to administrator
|
||||
|
||||
Verification VerificationMetadata // if set sent verification notification
|
||||
}
|
||||
|
||||
@@ -82,9 +84,13 @@ func (s *Service) Submit(req Request) {
|
||||
if s.dataService != nil && req.Comment.ParentID != "" {
|
||||
if p, err := s.dataService.Get(req.Comment.Locator, req.Comment.ParentID, store.User{}); err == nil {
|
||||
req.parent = p
|
||||
req.Email, err = s.dataService.GetUserEmail(req.Comment.Locator.SiteID, p.User.ID)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] can't read email for %s, %v", p.User.ID, err)
|
||||
// user notification, should fetch email for it.
|
||||
// administrator notification comes with pre-set email
|
||||
if req.Email == "" {
|
||||
req.Email, err = s.dataService.GetUserEmail(req.Comment.Locator.SiteID, p.User.ID)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] can't read email for %s, %v", p.User.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,11 @@ func (t *Telegram) Send(ctx context.Context, req Request) error {
|
||||
// verification request received, send nothing
|
||||
return nil
|
||||
}
|
||||
if req.ForAdmin {
|
||||
// request for administrator received, do nothing with it
|
||||
// as we already sent message on request without this flag set
|
||||
return nil
|
||||
}
|
||||
client := http.Client{Timeout: telegramTimeOut}
|
||||
log.Printf("[DEBUG] send telegram notification to %s, comment id %s", t.channelID, req.Comment.ID)
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ type Rest struct {
|
||||
AnonVote bool
|
||||
WebRoot string
|
||||
RemarkURL string
|
||||
AdminEmail string
|
||||
ReadOnlyAge int
|
||||
SharedSecret string
|
||||
ScoreThresholds struct {
|
||||
@@ -364,6 +365,7 @@ func (s *Rest) controllerGroups() (public, private, admin, rss) {
|
||||
authenticator: s.Authenticator,
|
||||
notifyService: s.NotifyService,
|
||||
remarkURL: s.RemarkURL,
|
||||
adminEmail: s.AdminEmail,
|
||||
anonVote: s.AnonVote,
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ type private struct {
|
||||
notifyService *notify.Service
|
||||
authenticator *auth.Service
|
||||
remarkURL string
|
||||
adminEmail string
|
||||
anonVote bool
|
||||
}
|
||||
|
||||
@@ -131,9 +132,14 @@ func (s *private) createCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
s.cache.Flush(cache.Flusher(comment.Locator.SiteID).
|
||||
Scopes(comment.Locator.URL, lastCommentsScope, comment.User.ID, comment.Locator.SiteID))
|
||||
|
||||
// user notification
|
||||
if s.notifyService != nil {
|
||||
s.notifyService.Submit(notify.Request{Comment: finalComment})
|
||||
}
|
||||
// admin notification
|
||||
if s.notifyService != nil && s.adminEmail != "" {
|
||||
s.notifyService.Submit(notify.Request{Comment: finalComment, Email: s.adminEmail, ForAdmin: true})
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] created commend %+v", finalComment)
|
||||
|
||||
|
||||
@@ -596,11 +596,12 @@ func TestRest_EmailNotification(t *testing.T) {
|
||||
parentComment := store.Comment{}
|
||||
require.NoError(t, render.DecodeJSON(strings.NewReader(string(body)), &parentComment))
|
||||
// wait for mock notification Submit to kick off
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
require.Equal(t, 1, len(mockDestination.Get()))
|
||||
assert.Equal(t, "", mockDestination.Get()[0].Email)
|
||||
time.Sleep(time.Millisecond * 30)
|
||||
require.Equal(t, 2, len(mockDestination.Get()))
|
||||
assert.Empty(t, mockDestination.Get()[0].Email)
|
||||
assert.Equal(t, "admin@example.org", mockDestination.Get()[1].Email)
|
||||
|
||||
// create child comment from another user, no email notification expected
|
||||
// create child comment from another user, email notification only to admin expected
|
||||
req, err = http.NewRequest("POST", ts.URL+"/api/v1/comment", strings.NewReader(fmt.Sprintf(
|
||||
`{"text": "test 456",
|
||||
"pid": "%s",
|
||||
@@ -615,9 +616,10 @@ func TestRest_EmailNotification(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusCreated, resp.StatusCode, string(body))
|
||||
// wait for mock notification Submit to kick off
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
require.Equal(t, 2, len(mockDestination.Get()))
|
||||
assert.Empty(t, mockDestination.Get()[1].Email)
|
||||
time.Sleep(time.Millisecond * 30)
|
||||
require.Equal(t, 4, len(mockDestination.Get()))
|
||||
assert.Empty(t, mockDestination.Get()[2].Email)
|
||||
assert.Equal(t, "admin@example.org", mockDestination.Get()[3].Email)
|
||||
|
||||
// send confirmation token for email
|
||||
req, err = http.NewRequest(http.MethodPost, ts.URL+"/api/v1/email/subscribe?site=remark42&address=good@example.com", nil)
|
||||
@@ -629,10 +631,10 @@ func TestRest_EmailNotification(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode, string(body))
|
||||
// wait for mock notification Submit to kick off
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
require.Equal(t, 3, len(mockDestination.Get()))
|
||||
require.NotEmpty(t, mockDestination.Get()[2].Verification)
|
||||
verificationToken := mockDestination.Get()[2].Verification.Token
|
||||
time.Sleep(time.Millisecond * 30)
|
||||
require.Equal(t, 5, len(mockDestination.Get()))
|
||||
require.NotEmpty(t, mockDestination.Get()[4].Verification)
|
||||
verificationToken := mockDestination.Get()[4].Verification.Token
|
||||
|
||||
// verify email
|
||||
req, err = http.NewRequest(http.MethodPost, ts.URL+fmt.Sprintf("/api/v1/email/confirm?site=remark42&tkn=%s", verificationToken), nil)
|
||||
@@ -674,9 +676,9 @@ func TestRest_EmailNotification(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusCreated, resp.StatusCode, string(body))
|
||||
// wait for mock notification Submit to kick off
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
require.Equal(t, 4, len(mockDestination.Get()))
|
||||
assert.Equal(t, "good@example.com", mockDestination.Get()[3].Email)
|
||||
time.Sleep(time.Millisecond * 30)
|
||||
require.Equal(t, 7, len(mockDestination.Get()))
|
||||
assert.Equal(t, "good@example.com", mockDestination.Get()[5].Email)
|
||||
|
||||
// delete user's email
|
||||
req, err = http.NewRequest(http.MethodDelete, ts.URL+"/api/v1/email?site=remark42", nil)
|
||||
@@ -688,7 +690,7 @@ func TestRest_EmailNotification(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode, string(body))
|
||||
|
||||
// create child comment from another user, no email notification expected
|
||||
// create child comment from another user, no email notification expected except for admin
|
||||
req, err = http.NewRequest("POST", ts.URL+"/api/v1/comment", strings.NewReader(
|
||||
`{"text": "test 321",
|
||||
"user": {"name": "other_user"},
|
||||
@@ -702,9 +704,9 @@ func TestRest_EmailNotification(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusCreated, resp.StatusCode, string(body))
|
||||
// wait for mock notification Submit to kick off
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
require.Equal(t, 5, len(mockDestination.Get()))
|
||||
assert.Empty(t, mockDestination.Get()[4].Email)
|
||||
time.Sleep(time.Millisecond * 30)
|
||||
require.Equal(t, 9, len(mockDestination.Get()))
|
||||
assert.Empty(t, mockDestination.Get()[7].Email)
|
||||
}
|
||||
|
||||
func TestRest_UserAllData(t *testing.T) {
|
||||
|
||||
@@ -368,9 +368,10 @@ func startupT(t *testing.T) (ts *httptest.Server, srv *Rest, teardown func()) {
|
||||
SecretReader: token.SecretFunc(func() (string, error) { return "secret", nil }),
|
||||
AvatarStore: avatar.NewLocalFS(tmp + "/ava-remark42"),
|
||||
}),
|
||||
Cache: memCache,
|
||||
WebRoot: tmp,
|
||||
RemarkURL: "https://demo.remark42.com",
|
||||
Cache: memCache,
|
||||
WebRoot: tmp,
|
||||
RemarkURL: "https://demo.remark42.com",
|
||||
AdminEmail: "admin@example.org",
|
||||
ImageService: image.NewService(&image.FileSystem{
|
||||
Location: tmp + "/pics-remark42",
|
||||
Partitions: 100,
|
||||
|
||||
@@ -45,6 +45,8 @@ services:
|
||||
- NOTIFY_TELEGRAM_TOKEN
|
||||
- NOTIFY_TELEGRAM_CHAN
|
||||
- NOTIFY_EMAIL_FROM
|
||||
- ADMIN_SHARED_EMAIL
|
||||
- NOTIFY_EMAIL_ADMIN
|
||||
- SMTP_HOST
|
||||
- SMTP_USERNAME
|
||||
- SMTP_PASSWORD
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
- [How to configure remark42 without a subdomain](subdomain.md) with Nginx or Caddy
|
||||
- [Telegram notifications](telegram.md)
|
||||
- [Setup email authentication and\or email notifications](email.md)
|
||||
- [How to add new translation to remark42](translation.md)
|
||||
|
||||
@@ -101,6 +101,9 @@ Here is the list of variables which affect email notifications:
|
||||
NOTIFY_TYPE
|
||||
NOTIFY_EMAIL_FROM
|
||||
NOTIFY_EMAIL_VERIFICATION_SUBJ
|
||||
# for administrator notifications for new comments on their site
|
||||
ADMIN_SHARED_EMAIL
|
||||
NOTIFY_EMAIL_ADMIN
|
||||
```
|
||||
|
||||
After `SMTP_` variables are set, you can allow email notifications by setting these two variables:
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
## How to add new language translation to Remark42
|
||||
|
||||
Translation files are stored in [/frontend/app/locales](https://github.com/umputun/remark42/tree/master/frontend/app/locales)
|
||||
directory with `.json` extension and content like following:
|
||||
|
||||
```json
|
||||
{
|
||||
"anonymousLoginForm.length-limit": "Username must be at least 3 characters long",
|
||||
"anonymousLoginForm.log-in": "Log in",
|
||||
"anonymousLoginForm.symbol-limit": "Username must start from the letter and contain only latin letters, numbers, underscores, and spaces",
|
||||
<...>
|
||||
}
|
||||
```
|
||||
|
||||
### How to add a new translation
|
||||
|
||||
We truly appreciate people spending time contributing their translations to remark42. Please go through the steps
|
||||
below in order to have your translation start being available to all remark42 users and included in the next release.
|
||||
|
||||
1. create a fork of [umputun/remark42](https://github.com/umputun/remark42) repo, and if you already have one please
|
||||
pull the latest changes from the upstream master branch. It could be done like that:
|
||||
```shell
|
||||
git remote add upstream https://github.com/umputun/remark42.git
|
||||
git fetch upstream
|
||||
git rebase upstream/master
|
||||
git push
|
||||
```
|
||||
1. add new locale with [two-letter code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)
|
||||
of the language you want to make the translation into to list in
|
||||
[frontend/tasks/supportedLocales.json](https://github.com/umputun/remark42/blob/master/frontend/tasks/supportedLocales.json)
|
||||
1. run `npm run generate-langs` in `frontend` folder
|
||||
1. translate all values in the newly created json file in
|
||||
[frontend/app/locales/](https://github.com/umputun/remark42/blob/master/frontend/app/locales/)
|
||||
1. commit all changes above in your fork
|
||||
1. test your changes in the interface:
|
||||
1. uncomment `locale: "ru"` line in [frontend/index.ejs](https://github.com/umputun/remark42/blob/master/frontend/index.ejs#L133)
|
||||
and replace `ru` with your translation language code
|
||||
1. [run remark42 in Docker](https://github.com/umputun/remark42#development) by issuing following commands
|
||||
from the root directory of your remark42 fork:
|
||||
|
||||
```shell
|
||||
docker-compose -f compose-dev-frontend.yml build
|
||||
docker-compose -f compose-dev-frontend.yml up
|
||||
```
|
||||
1. open [http://127.0.0.1:8080](http://127.0.0.1:8080), log in, make a comment, make a reply to a comment,
|
||||
and make sure that your translation looks as you expect it to look
|
||||
1. make a screenshot from [http://127.0.0.1:8080](http://127.0.0.1:8080) with your translation in place
|
||||
1. after all previous steps are done, create a [Pull Request](https://github.com/umputun/remark42/pulls) to umputun/remark42
|
||||
repo with your changes, attaching a screenshot or two from your local test instance to it
|
||||
+2
-5
@@ -28,12 +28,9 @@
|
||||
- tests are running on push attempt
|
||||
- example tests can be found in `./app/store/user/reducers.test.ts`, `./app/components/auth-panel/auth-panel.test.tsx`
|
||||
|
||||
### how to add new locale.
|
||||
### How to add new locale
|
||||
|
||||
- add new item to `./tasks/supportedLocales.json`
|
||||
- run `npm run generate-langs`
|
||||
- commit all changed files
|
||||
- translate all string in new generated dictionary `./app/locale/<new-locale>.json`
|
||||
Please see [this documentation](https://github.com/umputun/remark42/blob/master/docs/translation.md).
|
||||
|
||||
### Notes
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import jestFetchMock from 'jest-fetch-mock';
|
||||
|
||||
import { emailVerificationForSubscribe } from './api';
|
||||
|
||||
jest.mock('@app/common/constants', () => ({
|
||||
BASE_URL: 'https://example.com',
|
||||
API_BASE: '/api',
|
||||
}));
|
||||
|
||||
jest.mock('@app/common/settings', () => ({
|
||||
siteId: 'remark42',
|
||||
}));
|
||||
|
||||
describe('api', () => {
|
||||
beforeAll(() => {
|
||||
jestFetchMock.enableMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jestFetchMock.disableMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jestFetchMock.resetMocks();
|
||||
});
|
||||
it('should send request with encoded email', async () => {
|
||||
await emailVerificationForSubscribe("address.!#$%&'*+-/=?^_`{|}~(),:;<>[\\]@example.com");
|
||||
|
||||
expect(jestFetchMock.mock.calls.length).toEqual(1);
|
||||
|
||||
const url = jestFetchMock.mock.calls[0][0] as string;
|
||||
const match = url.match(/address=(\S+)$/);
|
||||
|
||||
expect(match).toBeArray();
|
||||
expect((match as string[]).length).toBeGreaterThan(1);
|
||||
expect((match as string[])[1]).toBe(
|
||||
"address.!%23%24%25%26'*%2B-%2F%3D%3F%5E_%60%7B%7C%7D~()%2C%3A%3B%3C%3E%5B%5C%5D%40example.com"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -277,7 +277,7 @@ export const uploadImage = (image: File): Promise<Image> => {
|
||||
*/
|
||||
export const emailVerificationForSubscribe = (emailAddress: string) =>
|
||||
fetcher.post({
|
||||
url: `/email/subscribe?site=${siteId}&address=${emailAddress}`,
|
||||
url: `/email/subscribe?site=${siteId}&address=${encodeURIComponent(emailAddress)}`,
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ export class AuthPanel extends Component<Props, State> {
|
||||
href={`${window.location.origin}/web/comments.html${window.location.search}`}
|
||||
target="_blank"
|
||||
>
|
||||
new page
|
||||
<FormattedMessage id="authPanel.new-page" defaultMessage="new page" />
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -26,8 +26,7 @@ export const messages = defineMessages({
|
||||
},
|
||||
symbolLimit: {
|
||||
id: 'anonymousLoginForm.symbol-limit',
|
||||
defaultMessage:
|
||||
'Username must start from the letter and contain only latin letters, numbers, underscores, and spaces',
|
||||
defaultMessage: 'Username must start with a letter and contain only latin letters, numbers, underscores, or spaces',
|
||||
},
|
||||
userName: {
|
||||
id: 'anonymousLoginForm.user-name',
|
||||
|
||||
@@ -26,7 +26,7 @@ function simulateInput(input: ReactWrapper, value: string) {
|
||||
|
||||
describe('EmailLoginForm', () => {
|
||||
const testUser = ({} as any) as User;
|
||||
const onSuccess = jest.fn(async () => {});
|
||||
const onSuccess = jest.fn(async () => undefined);
|
||||
const onSignIn = jest.fn(async () => testUser);
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
+1
-1
@@ -105,7 +105,7 @@ const renderTokenPart = (
|
||||
setEmailStep: () => void
|
||||
) => (
|
||||
<Fragment>
|
||||
<Button kind="link" mix="auth-panel-email-login-form__back-button" {...getHandleClickProps(setEmailStep)}>
|
||||
<Button kind="link" mix="auth-email-login-form__back-button" {...getHandleClickProps(setEmailStep)}>
|
||||
<FormattedMessage id="subscribeByEmail.back" defaultMessage="Back" />
|
||||
</Button>
|
||||
<TextareaAutosize
|
||||
|
||||
@@ -88,7 +88,7 @@ export const TextExpander: FunctionalComponent = ({ children }) => {
|
||||
expander.removeEventListener('text-expander-value', textExpanderValueListener);
|
||||
};
|
||||
}
|
||||
return () => {};
|
||||
return () => undefined;
|
||||
}, [theme]);
|
||||
if (StaticStore.config.emoji_enabled) {
|
||||
return <text-expander ref={expanderRef}>{children}</text-expander>;
|
||||
|
||||
@@ -146,7 +146,7 @@ describe('<Comment />', () => {
|
||||
});
|
||||
|
||||
it('disabled for already upvoted comment', async () => {
|
||||
const voteSpy = jest.fn(async () => {});
|
||||
const voteSpy = jest.fn(async () => undefined);
|
||||
const element = mount(
|
||||
<Comment
|
||||
{...(DefaultProps as Props)}
|
||||
@@ -180,7 +180,7 @@ describe('<Comment />', () => {
|
||||
}, 30000);
|
||||
|
||||
it('disabled for already downvoted comment', async () => {
|
||||
const voteSpy = jest.fn(async () => {});
|
||||
const voteSpy = jest.fn(async () => undefined);
|
||||
const element = mount(
|
||||
<Comment
|
||||
{...(DefaultProps as Props)}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"authPanel.logged-as": "Du hast dich angemeldet als",
|
||||
"authPanel.login": "Anmelden:",
|
||||
"authPanel.logout": "Abmelden?",
|
||||
"authPanel.new-page": "neue Seite",
|
||||
"authPanel.or-provider": "oder",
|
||||
"authPanel.other-provider": "Andere",
|
||||
"authPanel.read-only": "Nur-Lesen",
|
||||
@@ -52,7 +53,7 @@
|
||||
"comment.verified-user": "Bestätigter Benutzer",
|
||||
"comment.verify-user": "Möchtest du den Benutzer {userName} wirklich bestätigen?",
|
||||
"comment.vote-error": "Fehler beim Abstimmen: {voteErrorMessage}",
|
||||
"commentForm.anonymous-uploading-disabled": "Image uploading is disabled for unauthorized users. You should login before uploading.",
|
||||
"commentForm.anonymous-uploading-disabled": "Der Upload von Bildern ist nur für registrierte Benutzer möglich. Bitte melde dich zuvor an.",
|
||||
"commentForm.exceeded-size": "Die Datei {fileName} überschreitet das Größenlimit von {maxImageSize}",
|
||||
"commentForm.input-placeholder": "Gib hier deinen Kommentar ein",
|
||||
"commentForm.new-comment": "Neuer Kommentar",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"anonymousLoginForm.length-limit": "Username must be at least 3 characters long",
|
||||
"anonymousLoginForm.log-in": "Log in",
|
||||
"anonymousLoginForm.symbol-limit": "Username must start from the letter and contain only latin letters, numbers, underscores, and spaces",
|
||||
"anonymousLoginForm.symbol-limit": "Username must start with a letter and contain only latin letters, numbers, underscores, or spaces",
|
||||
"anonymousLoginForm.user-name": "Username",
|
||||
"authPanel.anonymous-provider": "Anonymous",
|
||||
"authPanel.disable-comments": "Disable comments",
|
||||
@@ -12,6 +12,7 @@
|
||||
"authPanel.logged-as": "You logged in as",
|
||||
"authPanel.login": "Login:",
|
||||
"authPanel.logout": "Logout?",
|
||||
"authPanel.new-page": "new page",
|
||||
"authPanel.or-provider": "or",
|
||||
"authPanel.other-provider": "Other",
|
||||
"authPanel.read-only": "Read-only",
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
{
|
||||
"anonymousLoginForm.length-limit": "El nombre de usuario debe ser de al menos 3 caracteres de largo",
|
||||
"anonymousLoginForm.log-in": "Acceder",
|
||||
"anonymousLoginForm.symbol-limit": "El nombre de usuario debe comenzar con una letra y contener solamente letras latinas, números, guión bajo o espacio",
|
||||
"anonymousLoginForm.user-name": "Nombre de usuario",
|
||||
"authPanel.anonymous-provider": "Anónimo",
|
||||
"authPanel.disable-comments": "Deshabilitar comentarios",
|
||||
"authPanel.disabled-cookies": "Deshabilita las cookies de terceros que bloquean el acceso o abre los comentarios en una",
|
||||
"authPanel.enable-comments": "Habilitar comentarios",
|
||||
"authPanel.enable-cookies": "Habilitar cookies para acceder y comentar",
|
||||
"authPanel.hide-settings": "Ocultar opciones",
|
||||
"authPanel.logged-as": "Accediste como",
|
||||
"authPanel.login": "Acceder:",
|
||||
"authPanel.logout": "¿Salir?",
|
||||
"authPanel.new-page": "nueva página",
|
||||
"authPanel.or-provider": "o",
|
||||
"authPanel.other-provider": "Otro",
|
||||
"authPanel.read-only": "Solo lectura",
|
||||
"authPanel.request-to-delete-data": "Solicitar la eliminación de mis datos",
|
||||
"authPanel.show-settings": "Mostrar opciones",
|
||||
"blockingDuration.day": "Por un día",
|
||||
"blockingDuration.month": "Por un mes",
|
||||
"blockingDuration.permanently": "Permanentemente",
|
||||
"blockingDuration.week": "Por una semana",
|
||||
"comment.block": "Bloquear",
|
||||
"comment.block-user": "¿Quieres bloquear a {userName} {duration}?",
|
||||
"comment.blocked-user": "Bloqueado",
|
||||
"comment.blocking-period": "Período de bloqueo",
|
||||
"comment.cancel": "Cancelar",
|
||||
"comment.controversy": "Controversia: {value}",
|
||||
"comment.copied": "¡Copiado!",
|
||||
"comment.copy": "Copiar",
|
||||
"comment.delete": "Eliminar",
|
||||
"comment.delete-message": "¿Quieres eliminar este comentario?",
|
||||
"comment.deleted-comment": "Este comentario fue eliminado",
|
||||
"comment.deleted-user": "Eliminado",
|
||||
"comment.edit": "Editar",
|
||||
"comment.expired-time": "El tiempo de edición ha expirado.",
|
||||
"comment.go-to-parent": "Ir al comentario padre",
|
||||
"comment.hide": "Ocultar",
|
||||
"comment.hide-user-comment": "¿Quieres ocultar los comentarios de {userName}?",
|
||||
"comment.pin": "Anclar",
|
||||
"comment.pin-comment": "¿Quieres anclar este comentario?",
|
||||
"comment.reply": "Responder",
|
||||
"comment.time": "{day} a las {time}",
|
||||
"comment.toggle-verification": "Alternar verificación",
|
||||
"comment.unblock": "Desbloquear",
|
||||
"comment.unblock-user": "¿Quieres desbloquear a este usuario?",
|
||||
"comment.unpin": "Desanclar",
|
||||
"comment.unpin-comment": "¿Quieres desanclar este comentario?",
|
||||
"comment.unverified-user": "Usuario no verificado",
|
||||
"comment.unverify-user": "¿Quieres quitar la verificación a {userName}?",
|
||||
"comment.verified-user": "Usuario verificado",
|
||||
"comment.verify-user": "¿Quieres verificar a {userName}?",
|
||||
"comment.vote-error": "Error al votar: {voteErrorMessage}",
|
||||
"commentForm.anonymous-uploading-disabled": "La subida de imágenes está deshabilitada para usuarios no autenticados. Deberías acceder antes de subir imágenes.",
|
||||
"commentForm.exceeded-size": "{fileName} excede el tamaño máximo de {maxImageSize}",
|
||||
"commentForm.input-placeholder": "Tu comentario aquí",
|
||||
"commentForm.new-comment": "Nuevo comentario",
|
||||
"commentForm.notice-about-styling": "El formato con <a>Markdown</a> está soportado",
|
||||
"commentForm.preview": "Pre-visualizar",
|
||||
"commentForm.reply": "Responder",
|
||||
"commentForm.save": "Guardar",
|
||||
"commentForm.send": "Enviar",
|
||||
"commentForm.subscribe-by": "Suscribirse por",
|
||||
"commentForm.subscribe-or": "o",
|
||||
"commentForm.unexpected-error": "Algo salió mal. Por favor vuelve a intentar más tarde.",
|
||||
"commentForm.upload-file-fail": "la subida de {fileName} falló con \"{errorMessage}\"",
|
||||
"commentForm.uploading": "Subiendo...",
|
||||
"commentForm.uploading-file": "subiendo {fileName}...",
|
||||
"commentSort.sort-by": "Ordernar por",
|
||||
"commentsSort.best": "Mejor",
|
||||
"commentsSort.least-controversial": "Menos controversial",
|
||||
"commentsSort.least-recently-updated": "Actualizado menos recientemente",
|
||||
"commentsSort.most-controversial": "Más controversial",
|
||||
"commentsSort.newest": "Más nuevo",
|
||||
"commentsSort.oldest": "Más antiguo",
|
||||
"commentsSort.recently-updated": "Actualizado más recientemente",
|
||||
"commentsSort.worst": "Peor",
|
||||
"emailLoginForm.back": "Volver",
|
||||
"emailLoginForm.confirm": "Confirmar",
|
||||
"emailLoginForm.email-address": "Dirección de correo electrónico",
|
||||
"emailLoginForm.empty-token": "El campo de token no debe ser vacío",
|
||||
"emailLoginForm.expired-token": "El token ha expirado",
|
||||
"emailLoginForm.invalid-email": "La dirección de correo electrónica no es válida",
|
||||
"emailLoginForm.loading": "Cargando...",
|
||||
"emailLoginForm.send-verification": "Enviar verificación",
|
||||
"emailLoginForm.token": "Token",
|
||||
"emailLoginForm.user-not-found": "No se encontró el usuario",
|
||||
"errors.0": "Algo salió mal. Por favor vuelve a intentar más tarde.",
|
||||
"errors.1": "No se ha encontrado el comentario. Por favor refresca la página y vuelve a intentar.",
|
||||
"errors.10": "Es muy tarde para editar el comentario.",
|
||||
"errors.11": "El comentario ya tiene una respuesta. No es posible editarlo.",
|
||||
"errors.12": "No se ha podido guardar el resultado del voto. Por favor vuelve a intentar más tarde.",
|
||||
"errors.13": "No puedes votar tu propio comentario.",
|
||||
"errors.14": "Ya has votado el comentario.",
|
||||
"errors.15": "Demasiados votos para el comentario.",
|
||||
"errors.16": "Ya se ha alcanzado el puntaje mínimo para el comentario.",
|
||||
"errors.17": "Acción rechazada. Por favor vuelve a intentar más tarde.",
|
||||
"errors.18": "No se ha encontrado el archivo solicitado.",
|
||||
"errors.2": "No se ha podido deserializar la petición entrante.",
|
||||
"errors.3": "No tienes permisos para esta operación.",
|
||||
"errors.4": "Datos de comentario inválidos.",
|
||||
"errors.5": "El comentario no se ha encontrado. Por favor refresca la página y vuelve a intentar.",
|
||||
"errors.6": "El sitio no se ha encontrado. Por favor refresca la página y vuelve a intentar.",
|
||||
"errors.7": "El usuario ha sido bloqueado.",
|
||||
"errors.8": "El usuario ha sido bloqueado.",
|
||||
"errors.9": "No se ha podido cambiar el comentario. Por favor vuelve a intentar más tarde.",
|
||||
"errors.failed-fetch": "No se ha podido obtener. Por favor revisa tu conexión a internet o vuelve a intentar más tarde",
|
||||
"errors.forbidden": "Prohibido.",
|
||||
"errors.not-authorized": "No autorizado.",
|
||||
"errors.to-many-request": "Has llegado al límite de peticiones.",
|
||||
"errors.unexpected-error": "Algo salió mal.",
|
||||
"root.pinned-comments": "Comentarios anclados",
|
||||
"root.powered-by": "Powered by <a>Remark42</a>",
|
||||
"root.show-more": "Mostrar más",
|
||||
"settings.block": "bloquear",
|
||||
"settings.block-time": "hasta el {day} a las {time}",
|
||||
"settings.block-user": "¿Quieres bloquear a {userName}?",
|
||||
"settings.blocked-users-header": "Usuarios bloqueados:",
|
||||
"settings.blocked-users-title": "Usuarios bloqueados",
|
||||
"settings.hidden-user-header": "Usuarios ocultos:",
|
||||
"settings.hidden-users-title": "Usuarios ocultos",
|
||||
"settings.hide": "ocultar",
|
||||
"settings.no-blocked-users": "No hay usuarios bloqueados.",
|
||||
"settings.no-hidden-users": "No hay usuarios ocultos.",
|
||||
"settings.permanently": "permanentemente",
|
||||
"settings.show": "mostrar",
|
||||
"settings.unblock": "desbloquear",
|
||||
"settings.unblock-user": "¿Quieres desbloquear a {userName}?",
|
||||
"settings.unknown": "desconocido",
|
||||
"subscribeByEmail.back": "Volver",
|
||||
"subscribeByEmail.close": "Cerrar",
|
||||
"subscribeByEmail.email": "Correo electrónico",
|
||||
"subscribeByEmail.expired-token": "Token expirado",
|
||||
"subscribeByEmail.have-been-subscribed": "Has sido suscripto a actualizaciones por correo electrónico",
|
||||
"subscribeByEmail.have-been-unsubscribed": "Has sido de-suscripto a actualizaciones por correo electrónico",
|
||||
"subscribeByEmail.only-registered-users": "Disponible solo para usuarios registrados",
|
||||
"subscribeByEmail.submit": "Enviar",
|
||||
"subscribeByEmail.subscribe": "Suscribir",
|
||||
"subscribeByEmail.subscribe-by-email": "Suscribir por correo electrónico",
|
||||
"subscribeByEmail.subscribe-to-replies": "Suscribir a respuestas",
|
||||
"subscribeByEmail.subscribed": "Estás suscripto a actualizaciones por correo electrónico",
|
||||
"subscribeByEmail.token": "Token",
|
||||
"subscribeByEmail.unsubscribe": "De-suscribir",
|
||||
"subscribeByRSS.button-title": "Suscribir por RSS",
|
||||
"subscribeByRSS.replies": "Respuestas",
|
||||
"subscribeByRSS.site": "Sitio",
|
||||
"subscribeByRSS.thread": "Hilo",
|
||||
"subscribeByRSS.title": "RSS",
|
||||
"toolbar.attach-image": "Adjunta la imágen, arrastra y suelta, o pega desde el portapapeles",
|
||||
"toolbar.bold": "Agrega texto en negrita <cmd-b>",
|
||||
"toolbar.code": "Inserta un código",
|
||||
"toolbar.header": "Agrega un título",
|
||||
"toolbar.italic": "Agrega texto en cursiva <cmd-i>",
|
||||
"toolbar.link": "Agrega un link <cmd-k>",
|
||||
"toolbar.ordered-list": "Agrega una lista numerada",
|
||||
"toolbar.quote": "Inserta una cita",
|
||||
"toolbar.unordered-list": "Agrega una lista sin numerar",
|
||||
"user-info.last-comments": "Últimos comentarios de {userName}",
|
||||
"user-info.unexpected-error": "Algo salió mal",
|
||||
"vote.anonymous": "Los usuarios anónimos no puede votar",
|
||||
"vote.deleted": "No se puede votar un comentario eliminado",
|
||||
"vote.guest": "Accede para votar",
|
||||
"vote.only-positive": "El puntaje debe ser positivo",
|
||||
"vote.only-post-page": "Solo se puede votar en la página de la publicación",
|
||||
"vote.own-comment": "No puedes votar tu propio comentario",
|
||||
"vote.readonly": "No se puede votar en tópicos de solo lectura"
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
"authPanel.logged-as": "Olet kirjautunut sisään nimellä",
|
||||
"authPanel.login": "Kirjaudu sisään:",
|
||||
"authPanel.logout": "Kirjaudu ulos?",
|
||||
"authPanel.new-page": "uusi sivu",
|
||||
"authPanel.or-provider": "tai",
|
||||
"authPanel.other-provider": "Muu",
|
||||
"authPanel.read-only": "Vain luku",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"authPanel.logged-as": "Вы вошли как",
|
||||
"authPanel.login": "Вход:",
|
||||
"authPanel.logout": "Выйти?",
|
||||
"authPanel.new-page": "новая страница",
|
||||
"authPanel.or-provider": "или",
|
||||
"authPanel.other-provider": "Другой",
|
||||
"authPanel.read-only": "Только для чтение",
|
||||
|
||||
@@ -70,12 +70,12 @@ describe('user', () => {
|
||||
);
|
||||
const dispatch = jest.fn();
|
||||
const getState = jest.fn();
|
||||
await logIn({ name: 'google' })(dispatch, getState, undefined).catch(() => {});
|
||||
await logIn({ name: 'google' })(dispatch, getState, undefined).catch(() => undefined);
|
||||
expect(dispatch).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should unset user on logOut', async () => {
|
||||
(api.logOut as any).mockImplementation(async (): Promise<void> => {});
|
||||
(api.logOut as any).mockImplementation(async (): Promise<void> => undefined);
|
||||
const dispatch = jest.fn();
|
||||
const getState = jest.fn();
|
||||
await logout()(dispatch, getState, undefined);
|
||||
|
||||
@@ -18,6 +18,11 @@ export async function loadLocale(locale: string): Promise<Record<string, string>
|
||||
.then(res => res.default)
|
||||
.catch(() => enMessages);
|
||||
}
|
||||
if (locale === 'es') {
|
||||
return import(/* webpackChunkName: "es" */ '../locales/es.json')
|
||||
.then(res => res.default)
|
||||
.catch(() => enMessages);
|
||||
}
|
||||
|
||||
return enMessages;
|
||||
}
|
||||
|
||||
Generated
+2012
-4440
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@
|
||||
"scripts": {
|
||||
"build": "webpack --mode production && es-check es5 './public/*.js'",
|
||||
"start": "webpack-dev-server --mode development",
|
||||
"check": "tsc -p tsconfig.json --noEmit --skipLibCheck",
|
||||
"check": "tsc -p tsconfig.typecheck.json --noEmit --skipLibCheck",
|
||||
"check:translation": "npm run extract-messages && node ./tasks/checkTranslation.js",
|
||||
"lint": "eslint --max-warnings=0 --ext=.ts,.tsx,.js,.jsx .",
|
||||
"lint:style": "stylelint '**/*.scss' '**/*.pcss' '**/*.css' 'iframe.html'",
|
||||
@@ -39,10 +39,10 @@
|
||||
"@types/fetch-mock": "^7.3.1",
|
||||
"@types/jest": "^24.0.20",
|
||||
"@types/lodash-es": "^4.17.3",
|
||||
"@types/node": "^12.11.7",
|
||||
"@types/node": "^12.12.34",
|
||||
"@types/react-redux": "^7.1.5",
|
||||
"@types/redux-mock-store": "^1.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^2.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "^2.27.0",
|
||||
"@typescript-eslint/parser": "^2.5.0",
|
||||
"autoprefixer": "^9.7.0",
|
||||
"babel-eslint": "^10.0.3",
|
||||
@@ -94,8 +94,8 @@
|
||||
"stylelint-config-standard": "^20.0.0",
|
||||
"stylelint-value-no-unknown-custom-properties": "^2.0.0",
|
||||
"ts-jest": "^25.2.1",
|
||||
"ts-loader": "^6.2.1",
|
||||
"typescript": "^3.6.4",
|
||||
"ts-loader": "^6.2.2",
|
||||
"typescript": "^3.8.3",
|
||||
"webpack": "^4.41.2",
|
||||
"webpack-bundle-analyzer": "^3.6.1",
|
||||
"webpack-cli": "^3.3.9",
|
||||
@@ -123,6 +123,7 @@
|
||||
"redux-thunk": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=12.11",
|
||||
"npm": ">=6.13.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
["en", "ru", "de", "fi"]
|
||||
["en", "ru", "de", "fi", "es"]
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"incremental": false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user