mirror of
https://github.com/wassname/talk.git
synced 2026-07-03 14:37:11 +08:00
Merge branch 'master' into schema-level-plugin-support
This commit is contained in:
@@ -193,7 +193,12 @@ export default {
|
||||
},
|
||||
updateQueries: {
|
||||
CoralEmbedStream_Embed: (prev, {mutationResult: {data: {createComment: {comment}}}}) => {
|
||||
if (prev.asset.settings.moderation === 'PRE' || comment.status === 'PREMOD' || comment.status === 'REJECTED' || comment.status === 'SYSTEM_WITHHELD') {
|
||||
if (
|
||||
prev.me.roles.indexOf('ADMIN') === -1 && prev.asset.settings.moderation === 'PRE' ||
|
||||
comment.status === 'PREMOD' ||
|
||||
comment.status === 'REJECTED' ||
|
||||
comment.status === 'SYSTEM_WITHHELD'
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
return insertCommentIntoEmbedQuery(prev, comment);
|
||||
|
||||
@@ -285,6 +285,7 @@ const fragments = {
|
||||
ignoredUsers {
|
||||
id
|
||||
}
|
||||
roles
|
||||
}
|
||||
settings {
|
||||
organizationName
|
||||
|
||||
@@ -49,8 +49,9 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => {
|
||||
memoized = null;
|
||||
resolvedDocument = null;
|
||||
lastNetworkStatus = null;
|
||||
data = null;
|
||||
name = '';
|
||||
apolloData = null;
|
||||
data = null;
|
||||
|
||||
// Pending subscription data.
|
||||
subscriptionQueue = [];
|
||||
@@ -151,6 +152,7 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => {
|
||||
};
|
||||
|
||||
nextData(data) {
|
||||
this.apolloData = data;
|
||||
this.emitWhenNeeded(data);
|
||||
|
||||
// If data was previously set, we update it in a immutable way.
|
||||
@@ -181,16 +183,24 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => {
|
||||
variables: data.variables,
|
||||
networkStatus: data.networkStatus,
|
||||
loading: data.loading,
|
||||
startPolling: data.startPolling,
|
||||
stopPolling: data.stopPolling,
|
||||
refetch: data.refetch,
|
||||
updateQuery: data.updateQuery,
|
||||
subscribeToMoreThrottled: this.subscribeToMoreThrottled,
|
||||
startPolling: (...args) => {
|
||||
return this.apolloData.startPolling(...args);
|
||||
},
|
||||
stopPolling: (...args) => {
|
||||
return this.apolloData.stopPolling(...args);
|
||||
},
|
||||
updateQuery: (...args) => {
|
||||
return this.apolloData.updateQuery(...args);
|
||||
},
|
||||
refetch: (...args) => {
|
||||
return this.apolloData.refetch(...args);
|
||||
},
|
||||
subscribeToMore: (stmArgs) => {
|
||||
const resolvedDocument = this.resolveDocument(stmArgs.document);
|
||||
|
||||
// Resolve document fragments before passing it to `apollo-client`.
|
||||
return data.subscribeToMore({
|
||||
return this.apolloData.subscribeToMore({
|
||||
...stmArgs,
|
||||
document: resolvedDocument,
|
||||
onError: (err) => {
|
||||
@@ -209,7 +219,7 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => {
|
||||
{variables: lmArgs.variables});
|
||||
|
||||
// Resolve document fragments before passing it to `apollo-client`.
|
||||
return data.fetchMore({
|
||||
return this.apolloData.fetchMore({
|
||||
...lmArgs,
|
||||
query: resolvedDocument,
|
||||
})
|
||||
|
||||
@@ -61,7 +61,7 @@ class ProfileContainer extends Component {
|
||||
return <NotLoggedIn showSignInDialog={showSignInDialog} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
if (loading || !me) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,13 @@ const CONFIG = {
|
||||
// WEBPACK indicates when webpack is currently building.
|
||||
WEBPACK: process.env.WEBPACK === 'TRUE',
|
||||
|
||||
// EMAIL_SUBJECT_PREFIX is the string before emails in the subject.
|
||||
EMAIL_SUBJECT_PREFIX: process.env.TALK_EMAIL_SUBJECT_PREFIX || '[Talk]',
|
||||
|
||||
// DEFAULT_LANG is the default language used for server sent emails and
|
||||
// rendered text.
|
||||
DEFAULT_LANG: process.env.TALK_DEFAULT_LANG || 'en',
|
||||
|
||||
// When TRUE, it ensures that database indexes created in core will not add
|
||||
// indexes.
|
||||
CREATE_MONGO_INDEXES: process.env.DISABLE_CREATE_MONGO_INDEXES !== 'TRUE',
|
||||
|
||||
@@ -460,4 +460,10 @@ Could be read as:
|
||||
## TALK_DISABLE_IGNORE_FLAGS_AGAINST_STAFF
|
||||
|
||||
When `TRUE`, staff members will have their accounts and comments moderated the
|
||||
same as any other user in the system. (Default `FALSE`)
|
||||
same as any other user in the system. (Default `FALSE`)
|
||||
|
||||
## TALK_EMAIL_SUBJECT_PREFIX
|
||||
|
||||
The prefix for the subject of emails sent. An email with the specified subject
|
||||
of `Email Confirmation` would then be sent as `[Talk] Email Confirmation`.
|
||||
(Default `[Talk]`)
|
||||
+1
-1
@@ -285,7 +285,7 @@ da:
|
||||
no_like_bio: "Jeg kan ikke lide denne biografi"
|
||||
no_like_username: "Jeg kan ikke lide dette brugernavn"
|
||||
other: "Andet"
|
||||
permalink: "Link"
|
||||
permalink: "Delen"
|
||||
personal_info: "Denne kommentar afslører personligt identificerbare oplysninger"
|
||||
post: "Post"
|
||||
profile: "Profil"
|
||||
|
||||
+6
-1
@@ -166,6 +166,11 @@ en:
|
||||
minute: "minute"
|
||||
minutes_plural: "minutes"
|
||||
email:
|
||||
suspended:
|
||||
subject: "Your account has been suspended"
|
||||
banned:
|
||||
subject: "Your account has been banned"
|
||||
body: "In accordance with The Coral Project’s community guidelines, your account has been banned. You are now longer allowed to comment, flag or engage with our community."
|
||||
confirm:
|
||||
has_been_requested: "A email confirmation has been requested for the following account:"
|
||||
to_confirm: "To confirm the account, please visit the following link:"
|
||||
@@ -297,7 +302,7 @@ en:
|
||||
no_like_bio: "I don't like this bio"
|
||||
no_like_username: "I don't like this username"
|
||||
other: Other
|
||||
permalink: Link
|
||||
permalink: Share
|
||||
personal_info: "This comment reveals personally identifiable information"
|
||||
post: Post
|
||||
profile: Profile
|
||||
|
||||
+1
-1
@@ -283,7 +283,7 @@ es:
|
||||
no_like_bio: "No me gusta esta biografia"
|
||||
no_like_username: "No me gusta este nombre de usuario"
|
||||
other: Otro
|
||||
permalink: Enlace
|
||||
permalink: Compartir
|
||||
personal_info: "Este comentario muestra información personal"
|
||||
post: Publicar
|
||||
profile: Perfil
|
||||
|
||||
+1
-1
@@ -237,7 +237,7 @@ fr:
|
||||
no_like_bio: "Je n'aime pas cette biographie"
|
||||
no_like_username: "Je n'aime pas ce nom d'utilisateur"
|
||||
other: Autre
|
||||
permalink: Lien
|
||||
permalink: Partager
|
||||
personal_info: "Ce commentaire révèle des informations personnelles identifiables"
|
||||
post: Publier
|
||||
profile: Profil
|
||||
|
||||
+1
-1
@@ -288,7 +288,7 @@ pt_BR:
|
||||
no_like_bio: "Eu não gosto dessa descrição de perfil"
|
||||
no_like_username: "Eu não gosto deste nome de usuário"
|
||||
other: Outro
|
||||
permalink: Link
|
||||
permalink: Compartilhar
|
||||
personal_info: "Este comentário revela informações de identificação pessoal"
|
||||
post: Publicar
|
||||
profile: Perfil
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
const i18n = require('../services/i18n');
|
||||
|
||||
module.exports = (req, res, next) => {
|
||||
res.locals.t = i18n.request(req);
|
||||
next();
|
||||
};
|
||||
@@ -87,11 +87,12 @@ export default (tag, options = {}) => hoistStatics((WrappedComponent) => {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {root, asset, comment, user, config} = this.props;
|
||||
const {root, asset, comment, user, config, ...rest} = this.props;
|
||||
|
||||
const alreadyTagged = isTagged(comment.tags, TAG);
|
||||
|
||||
return <WrappedComponent
|
||||
{...rest}
|
||||
root={root}
|
||||
asset={asset}
|
||||
comment={comment}
|
||||
@@ -134,9 +135,9 @@ export default (tag, options = {}) => hoistStatics((WrappedComponent) => {
|
||||
${fragments.comment ? fragments.comment : ''}
|
||||
`
|
||||
}),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withAddTag,
|
||||
withRemoveTag
|
||||
withRemoveTag,
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
);
|
||||
|
||||
WithTags.displayName = `WithTags(${getDisplayName(WrappedComponent)})`;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import {OPEN_FEATURED_DIALOG, CLOSE_FEATURED_DIALOG} from './constants';
|
||||
|
||||
export const openFeaturedDialog = (comment, asset) => ({
|
||||
type: OPEN_FEATURED_DIALOG,
|
||||
comment: {
|
||||
id: comment.id,
|
||||
tags: comment.tags,
|
||||
},
|
||||
asset: {
|
||||
id: asset.id,
|
||||
},
|
||||
});
|
||||
|
||||
export const closeFeaturedDialog = () => ({
|
||||
type: CLOSE_FEATURED_DIALOG,
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
.dialog {
|
||||
border: none;
|
||||
box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
|
||||
width: 400px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.header {
|
||||
color: black;
|
||||
font-size: 1.5em;
|
||||
font-weight: 500;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.close {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.cancel {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.perform {
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 6px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Dialog} from 'coral-ui';
|
||||
import styles from './FeaturedDialog.css';
|
||||
import {t} from 'plugin-api/beta/client/services';
|
||||
import Button from 'coral-ui/components/Button';
|
||||
|
||||
const FeaturedDialog = ({showFeaturedDialog, closeFeaturedDialog, postTag}) => {
|
||||
|
||||
const onPerform = async () => {
|
||||
await postTag();
|
||||
await closeFeaturedDialog();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
className={cn(styles.dialog, 'talk-featured-dialog')}
|
||||
id="talkFeaturedDialog"
|
||||
open={showFeaturedDialog}
|
||||
onCancel={closeFeaturedDialog} >
|
||||
<span className={styles.close} onClick={closeFeaturedDialog}>×</span>
|
||||
<h2 className={styles.header}>{t('talk-plugin-featured-comments.feature_comment')}</h2>
|
||||
<div className={styles.content}>
|
||||
{t('talk-plugin-featured-comments.are_you_sure')}
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
<Button
|
||||
className={cn(styles.cancel, 'talk-featured-dialog-button-cancel')}
|
||||
cStyle="cancel"
|
||||
onClick={closeFeaturedDialog}
|
||||
raised >
|
||||
{t('talk-plugin-featured-comments.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className={cn(styles.perform, 'talk-featured-dialog-button-confirm')}
|
||||
cStyle="black"
|
||||
onClick={onPerform}
|
||||
raised >
|
||||
{t('talk-plugin-featured-comments.yes_feature_comment')}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
FeaturedDialog.propTypes = {
|
||||
showFeaturedDialog: PropTypes.bool.isRequired,
|
||||
closeFeaturedDialog: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default FeaturedDialog;
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import styles from './ModTag.css';
|
||||
import {t} from 'plugin-api/beta/client/services';
|
||||
import {Icon} from 'plugin-api/beta/client/components/ui';
|
||||
import {getErrorMessages} from 'plugin-api/beta/client/utils';
|
||||
|
||||
export default class ModTag extends React.Component {
|
||||
constructor() {
|
||||
@@ -29,17 +29,12 @@ export default class ModTag extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
postTag = async () => {
|
||||
try {
|
||||
await this.props.postTag();
|
||||
}
|
||||
catch(err) {
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
}
|
||||
openFeaturedDialog = (comment, asset) => {
|
||||
this.props.openFeaturedDialog(comment, asset);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {alreadyTagged, deleteTag} = this.props;
|
||||
const {alreadyTagged, deleteTag, comment, asset} = this.props;
|
||||
|
||||
return alreadyTagged ? (
|
||||
<span className={cn(styles.tag, styles.featured)}
|
||||
@@ -51,10 +46,19 @@ export default class ModTag extends React.Component {
|
||||
</span>
|
||||
) : (
|
||||
<span className={cn(styles.tag, {[styles.featured]: alreadyTagged})}
|
||||
onClick={this.postTag} >
|
||||
onClick={() => this.openFeaturedDialog(comment, asset)} >
|
||||
<Icon name="star_outline" className={cn(styles.tagIcon)} />
|
||||
{alreadyTagged ? t('talk-plugin-featured-comments.featured') : t('talk-plugin-featured-comments.feature')}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ModTag.propTypes = {
|
||||
alreadyTagged: PropTypes.bool,
|
||||
deleteTag: PropTypes.func,
|
||||
notify: PropTypes.func,
|
||||
openFeaturedDialog: PropTypes.func,
|
||||
comment: PropTypes.object,
|
||||
asset: PropTypes.object,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
const prefix = 'TALK_FEATURED_COMMENTS_ACTIONS';
|
||||
|
||||
export const OPEN_FEATURED_DIALOG = `${prefix}_OPEN_FEATURED_DIALOG`;
|
||||
export const CLOSE_FEATURED_DIALOG = `${prefix}_CLOSE_FEATURED_DIALOG`;
|
||||
@@ -0,0 +1,23 @@
|
||||
import {compose} from 'react-apollo';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import FeaturedDialog from '../components/FeaturedDialog';
|
||||
import {withTags, connect} from 'plugin-api/beta/client/hocs';
|
||||
import {closeFeaturedDialog} from '../actions';
|
||||
|
||||
const mapStateToProps = ({talkPluginFeaturedComments: state}) => ({
|
||||
showFeaturedDialog: state.showFeaturedDialog,
|
||||
comment: state.comment,
|
||||
asset: state.asset,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
closeFeaturedDialog,
|
||||
}, dispatch);
|
||||
|
||||
const enhance = compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withTags('featured'),
|
||||
);
|
||||
|
||||
export default enhance(FeaturedDialog);
|
||||
@@ -2,11 +2,13 @@ import ModTag from '../components/ModTag';
|
||||
import {withTags, connect} from 'plugin-api/beta/client/hocs';
|
||||
import {gql, compose} from 'react-apollo';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {openFeaturedDialog} from '../actions';
|
||||
import {notify} from 'plugin-api/beta/client/actions/notification';
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
notify,
|
||||
openFeaturedDialog,
|
||||
}, dispatch);
|
||||
|
||||
const fragments = {
|
||||
@@ -24,4 +26,3 @@ const enhance = compose(
|
||||
);
|
||||
|
||||
export default enhance(ModTag);
|
||||
|
||||
|
||||
@@ -6,19 +6,22 @@ import update from 'immutability-helper';
|
||||
import ModTag from './containers/ModTag';
|
||||
import ModActionButton from './containers/ModActionButton';
|
||||
import ModSubscription from './containers/ModSubscription';
|
||||
import FeaturedDialog from './containers/FeaturedDialog';
|
||||
import {gql} from 'react-apollo';
|
||||
import reducer from './reducer';
|
||||
|
||||
import {findCommentInEmbedQuery} from 'coral-embed-stream/src/graphql/utils';
|
||||
import {prependNewNodes} from 'plugin-api/beta/client/utils';
|
||||
|
||||
export default {
|
||||
translations,
|
||||
reducer,
|
||||
slots: {
|
||||
streamTabsPrepend: [Tab],
|
||||
streamTabPanes: [TabPane],
|
||||
commentInfoBar: [Tag],
|
||||
moderationActions: [ModActionButton],
|
||||
adminModeration: [ModSubscription],
|
||||
adminModeration: [ModSubscription, FeaturedDialog],
|
||||
adminCommentInfoBar: [ModTag],
|
||||
},
|
||||
mutations: {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import {OPEN_FEATURED_DIALOG, CLOSE_FEATURED_DIALOG} from './constants';
|
||||
|
||||
const initialState = {
|
||||
showFeaturedDialog: false,
|
||||
comment: {
|
||||
id: null,
|
||||
tags: []
|
||||
},
|
||||
asset: {
|
||||
id: null,
|
||||
},
|
||||
};
|
||||
|
||||
export default function reducer(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case OPEN_FEATURED_DIALOG:
|
||||
return {
|
||||
...state,
|
||||
comment: action.comment,
|
||||
asset: action.asset,
|
||||
showFeaturedDialog: true,
|
||||
};
|
||||
case CLOSE_FEATURED_DIALOG:
|
||||
return {
|
||||
...state,
|
||||
featuredCommentId: null,
|
||||
showFeaturedDialog: false,
|
||||
};
|
||||
default :
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,10 @@ en:
|
||||
notify_self_featured: 'The comment from {0} is now featured and approved'
|
||||
notify_featured: '{0} featured and approved comment "{1}"'
|
||||
notify_unfeatured: '{0} unfeatured comment "{1}"'
|
||||
feature_comment: Feature comment?
|
||||
are_you_sure: Are you sure you would like to feature this comment?
|
||||
cancel: Cancel
|
||||
yes_feature_comment: Yes, feature comment
|
||||
es:
|
||||
talk-plugin-featured-comments:
|
||||
un_feature: Desmarcar
|
||||
@@ -17,3 +21,7 @@ es:
|
||||
featured_comments: Comentarios Remarcados
|
||||
go_to_conversation: Ir al comentario
|
||||
tooltip_description: Comentarios seleccionados por nuestro equipo que valen la pena ser leidos
|
||||
feature_comment: Destacar comentario?
|
||||
are_you_sure: Está seguro que desea destacar este comentario?
|
||||
cancel: Cancelar
|
||||
yes_feature_comment: Si, destacar comentario
|
||||
+7
-6
@@ -7,7 +7,7 @@ const debug = require('debug')('talk:routes');
|
||||
const enabled = require('debug').enabled;
|
||||
const errors = require('../errors');
|
||||
const express = require('express');
|
||||
const i18n = require('../services/i18n');
|
||||
const i18n = require('../middleware/i18n');
|
||||
const path = require('path');
|
||||
const plugins = require('../services/plugins');
|
||||
const pubsub = require('../middleware/pubsub');
|
||||
@@ -64,6 +64,9 @@ if (!DISABLE_STATIC_SERVER) {
|
||||
router.get('/embed.js.map', serveFile('../dist/embed.js.map'));
|
||||
}
|
||||
|
||||
// Add the i18n middleware to all routes.
|
||||
router.use(i18n);
|
||||
|
||||
//==============================================================================
|
||||
// STATIC ROUTES
|
||||
//==============================================================================
|
||||
@@ -116,19 +119,19 @@ if (process.env.NODE_ENV !== 'production') {
|
||||
});
|
||||
});
|
||||
|
||||
// GraphQL documention.
|
||||
// GraphQL documentation.
|
||||
router.get('/admin/docs', (req, res) => {
|
||||
res.render('admin/docs');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
router.use('/api/v1', require('./api'));
|
||||
|
||||
//==============================================================================
|
||||
// ROUTES
|
||||
//==============================================================================
|
||||
|
||||
router.use('/api/v1', require('./api'));
|
||||
|
||||
// Development routes.
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
router.use('/assets', staticTemplate, require('./assets'));
|
||||
@@ -184,8 +187,6 @@ router.use('/', (err, req, res, next) => {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
i18n.init(req);
|
||||
|
||||
if (err instanceof errors.APIError) {
|
||||
res.status(err.status);
|
||||
res.render('error', {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<p><%= t('email.confirm.has_been_requested') %> <b><%= email %></b>.</p>
|
||||
<p><%= t('email.confirm.to_confirm') %> <a href="<%= BASE_URL %>admin/confirm-email#<%= token %>">Confirm Email</a></p>
|
||||
<p><%= t('email.confirm.to_confirm') %> <a href="<%= BASE_URL %>admin/confirm-email#<%= token %>"><%= t('email.confirm.confirm_email') %></a></p>
|
||||
<p><%= t('email.confirm.if_you_did_not') %></p>
|
||||
|
||||
+77
-43
@@ -1,57 +1,91 @@
|
||||
const has = require('lodash/has');
|
||||
const get = require('lodash/get');
|
||||
|
||||
const yaml = require('yamljs');
|
||||
|
||||
const da = yaml.load('./locales/da.yml');
|
||||
const es = yaml.load('./locales/es.yml');
|
||||
const en = yaml.load('./locales/en.yml');
|
||||
const fr = yaml.load('./locales/fr.yml');
|
||||
const pt_BR = yaml.load('./locales/pt_BR.yml');
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const debug = require('debug')('talk:services:i18n');
|
||||
const accepts = require('accepts');
|
||||
const _ = require('lodash');
|
||||
const yaml = require('yamljs');
|
||||
const plugins = require('./plugins');
|
||||
const {DEFAULT_LANG} = require('../config');
|
||||
|
||||
// default language
|
||||
let defaultLanguage = 'en';
|
||||
let language = defaultLanguage;
|
||||
const languages = ['en', 'da', 'es', 'fr', 'pt_BR'];
|
||||
const resolve = (...paths) => path.resolve(path.join(__dirname, '..', 'locales', ...paths));
|
||||
|
||||
const translations = Object.assign(en, es, fr, pt_BR, da);
|
||||
// Load all the translations.
|
||||
let translations = fs.readdirSync(resolve())
|
||||
|
||||
// Resolve all the filenames relative the the locales directory.
|
||||
.map((filename) => resolve(filename))
|
||||
|
||||
// Translations are only yml/yaml files.
|
||||
.filter((filename) => /\.(yaml|yml)$/.test(filename))
|
||||
|
||||
// Load the translation files from disk.
|
||||
.map((filename) => fs.readFileSync(filename, 'utf8'))
|
||||
|
||||
// Load the translation files.
|
||||
.reduce((packs, contents) => {
|
||||
|
||||
const pack = yaml.parse(contents);
|
||||
|
||||
return _.merge(packs, pack);
|
||||
}, {});
|
||||
|
||||
// Create a list of all supported translations.
|
||||
const languages = Object.keys(translations);
|
||||
|
||||
let loadedPluginTranslations = false;
|
||||
const loadPluginTranslations = () => {
|
||||
if (loadedPluginTranslations) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Load the plugin translations.
|
||||
plugins.get('server', 'translations').forEach(({plugin, translations: filename}) => {
|
||||
debug(`added plugin '${plugin.name}'`);
|
||||
|
||||
const pack = yaml.parse(fs.readFileSync(filename, 'utf8'));
|
||||
|
||||
translations = _.merge(translations, pack);
|
||||
});
|
||||
|
||||
loadedPluginTranslations = true;
|
||||
};
|
||||
|
||||
const t = (language) => (key, ...replacements) => {
|
||||
|
||||
// Loads the translations into the translations array from plugins. This is
|
||||
// done lazily to ensure that we don't have an import cycle.
|
||||
loadPluginTranslations();
|
||||
|
||||
// Check if the translation exists on the object.
|
||||
if (_.has(translations[language], key)) {
|
||||
|
||||
// Get the translation value.
|
||||
let translation = _.get(translations[language], key);
|
||||
|
||||
// Replace any {n} with the arguments passed to this method.
|
||||
replacements.forEach((str, n) => {
|
||||
translation = translation.replace(new RegExp(`\\{${n}\\}`, 'g'), str);
|
||||
});
|
||||
|
||||
return translation;
|
||||
} else {
|
||||
console.warn(`${key} language key not set`);
|
||||
return key;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Exposes a service object to allow translations.
|
||||
* @type {Object}
|
||||
*/
|
||||
const i18n = {
|
||||
|
||||
/**
|
||||
* Create the new Task kue.
|
||||
*/
|
||||
init(req) {
|
||||
request(req) {
|
||||
const lang = accepts(req).language(languages);
|
||||
language = lang ? lang : defaultLanguage;
|
||||
},
|
||||
|
||||
/**
|
||||
* Translates a key.
|
||||
*/
|
||||
t(key, ...replacements) {
|
||||
|
||||
if (has(translations[language], key)) {
|
||||
|
||||
let translation = get(translations[language], key);
|
||||
|
||||
// replace any {n} with the arguments passed to this method
|
||||
replacements.forEach((str, i) => {
|
||||
translation = translation.replace(new RegExp(`\\{${i}\\}`, 'g'), str);
|
||||
});
|
||||
|
||||
return translation;
|
||||
} else {
|
||||
console.warn(`${key} language key not set`);
|
||||
return key;
|
||||
}
|
||||
const language = lang ? lang : DEFAULT_LANG;
|
||||
|
||||
return t(language);
|
||||
},
|
||||
t: t(DEFAULT_LANG),
|
||||
};
|
||||
|
||||
module.exports = i18n;
|
||||
|
||||
+6
-5
@@ -13,7 +13,8 @@ const {
|
||||
SMTP_USERNAME,
|
||||
SMTP_PORT,
|
||||
SMTP_PASSWORD,
|
||||
SMTP_FROM_ADDRESS
|
||||
SMTP_FROM_ADDRESS,
|
||||
EMAIL_SUBJECT_PREFIX,
|
||||
} = require('../config');
|
||||
|
||||
// load all the templates as strings
|
||||
@@ -95,12 +96,12 @@ const mailer = module.exports = {
|
||||
}
|
||||
|
||||
// Prefix the subject with `[Talk]`.
|
||||
subject = `[Talk] ${subject}`;
|
||||
subject = `${EMAIL_SUBJECT_PREFIX} ${subject}`;
|
||||
|
||||
attachLocals(locals);
|
||||
|
||||
// Attach the templating function.
|
||||
locals['t'] = i18n.t;
|
||||
// Attach the translation function.
|
||||
locals.t = i18n.t;
|
||||
|
||||
return Promise.all([
|
||||
|
||||
@@ -112,7 +113,7 @@ const mailer = module.exports = {
|
||||
])
|
||||
.then(([html, text]) => {
|
||||
|
||||
// Create the job.
|
||||
// Create the job.
|
||||
return mailer.task.create({
|
||||
title: 'Mail',
|
||||
message: {
|
||||
|
||||
+18
-21
@@ -24,6 +24,7 @@ const RECAPTCHA_INCORRECT_TRIGGER = 5; // after 3 incorrect attempts, recaptcha
|
||||
const ActionsService = require('./actions');
|
||||
const MailerService = require('./mailer');
|
||||
const Wordlist = require('./wordlist');
|
||||
const i18n = require('./i18n');
|
||||
const Domainlist = require('./domainlist');
|
||||
const {escapeRegExp} = require('./regex');
|
||||
|
||||
@@ -430,16 +431,14 @@ module.exports = class UsersService {
|
||||
if (status === 'BANNED') {
|
||||
let localProfile = user.profiles.find((profile) => profile.provider === 'local');
|
||||
if (localProfile) {
|
||||
const options =
|
||||
{
|
||||
template: 'banned', // needed to know which template to render!
|
||||
locals: { // specifies the template locals.
|
||||
body: 'In accordance with The Coral Project’s community guidelines, your account has been banned. You are now longer allowed to comment, flag or engage with our community.'
|
||||
},
|
||||
subject: 'Your account has been banned',
|
||||
to: localProfile.id // This only works if the user has registered via e-mail.
|
||||
// We may want a standard way to access a user's e-mail address in the future
|
||||
};
|
||||
const options = {
|
||||
template: 'banned',
|
||||
locals: {
|
||||
body: i18n.t('email.banned.body'),
|
||||
},
|
||||
subject: i18n.t('email.banned.subject'),
|
||||
to: localProfile.id
|
||||
};
|
||||
await MailerService.sendSimple(options);
|
||||
}
|
||||
}
|
||||
@@ -467,16 +466,14 @@ module.exports = class UsersService {
|
||||
if (message) {
|
||||
let localProfile = user.profiles.find((profile) => profile.provider === 'local');
|
||||
if (localProfile) {
|
||||
const options =
|
||||
{
|
||||
template: 'suspension', // needed to know which template to render!
|
||||
locals: { // specifies the template locals.
|
||||
body: message
|
||||
},
|
||||
subject: 'Your account has been suspended',
|
||||
to: localProfile.id // This only works if the user has registered via e-mail.
|
||||
// We may want a standard way to access a user's e-mail address in the future
|
||||
};
|
||||
const options = {
|
||||
template: 'suspension',
|
||||
locals: {
|
||||
body: message
|
||||
},
|
||||
subject: i18n.t('email.suspended.subject'),
|
||||
to: localProfile.id,
|
||||
};
|
||||
|
||||
await MailerService.sendSimple(options);
|
||||
}
|
||||
@@ -509,7 +506,7 @@ module.exports = class UsersService {
|
||||
locals: { // specifies the template locals.
|
||||
body: message
|
||||
},
|
||||
subject: 'Email Suspension',
|
||||
subject: i18n.t('email.suspended.subject'),
|
||||
to: localProfile.id // This only works if the user has registered via e-mail.
|
||||
// We may want a standard way to access a user's e-mail address in the future
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user