) : (
+ onClick={() => this.openFeaturedDialog(comment, asset)} >
{alreadyTagged ? t('talk-plugin-featured-comments.featured') : t('talk-plugin-featured-comments.feature')}
);
}
}
+
+ModTag.propTypes = {
+ alreadyTagged: PropTypes.bool,
+ deleteTag: PropTypes.func,
+ notify: PropTypes.func,
+ openFeaturedDialog: PropTypes.func,
+ comment: PropTypes.object,
+ asset: PropTypes.object,
+};
diff --git a/plugins/talk-plugin-featured-comments/client/constants.js b/plugins/talk-plugin-featured-comments/client/constants.js
new file mode 100644
index 000000000..912f6f002
--- /dev/null
+++ b/plugins/talk-plugin-featured-comments/client/constants.js
@@ -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`;
diff --git a/plugins/talk-plugin-featured-comments/client/containers/FeaturedDialog.js b/plugins/talk-plugin-featured-comments/client/containers/FeaturedDialog.js
new file mode 100644
index 000000000..0440e9d08
--- /dev/null
+++ b/plugins/talk-plugin-featured-comments/client/containers/FeaturedDialog.js
@@ -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);
diff --git a/plugins/talk-plugin-featured-comments/client/containers/ModTag.js b/plugins/talk-plugin-featured-comments/client/containers/ModTag.js
index 88aee73f6..35e7edfcd 100644
--- a/plugins/talk-plugin-featured-comments/client/containers/ModTag.js
+++ b/plugins/talk-plugin-featured-comments/client/containers/ModTag.js
@@ -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);
-
diff --git a/plugins/talk-plugin-featured-comments/client/index.js b/plugins/talk-plugin-featured-comments/client/index.js
index 7a0b9827d..304bff647 100644
--- a/plugins/talk-plugin-featured-comments/client/index.js
+++ b/plugins/talk-plugin-featured-comments/client/index.js
@@ -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: {
diff --git a/plugins/talk-plugin-featured-comments/client/reducer.js b/plugins/talk-plugin-featured-comments/client/reducer.js
new file mode 100644
index 000000000..eb6d302ff
--- /dev/null
+++ b/plugins/talk-plugin-featured-comments/client/reducer.js
@@ -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;
+ }
+}
diff --git a/plugins/talk-plugin-featured-comments/client/translations.yml b/plugins/talk-plugin-featured-comments/client/translations.yml
index ea504cc9e..460a76be9 100644
--- a/plugins/talk-plugin-featured-comments/client/translations.yml
+++ b/plugins/talk-plugin-featured-comments/client/translations.yml
@@ -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
\ No newline at end of file
diff --git a/routes/index.js b/routes/index.js
index 8fc9e8f3f..454e32f58 100644
--- a/routes/index.js
+++ b/routes/index.js
@@ -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', {
diff --git a/services/email/email-confirm.html.ejs b/services/email/email-confirm.html.ejs
index 8a1bd05f7..76a8ea47b 100644
--- a/services/email/email-confirm.html.ejs
+++ b/services/email/email-confirm.html.ejs
@@ -1,3 +1,3 @@
<%= t('email.confirm.has_been_requested') %> <%= email %>.
-<%= t('email.confirm.to_confirm') %> Confirm Email
+<%= t('email.confirm.to_confirm') %> <%= t('email.confirm.confirm_email') %>
<%= t('email.confirm.if_you_did_not') %>
diff --git a/services/i18n.js b/services/i18n.js
index 4c65ed1d4..cd1bacd93 100644
--- a/services/i18n.js
+++ b/services/i18n.js
@@ -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;
diff --git a/services/mailer.js b/services/mailer.js
index a4f694b30..4433b9ef5 100644
--- a/services/mailer.js
+++ b/services/mailer.js
@@ -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: {
diff --git a/services/users.js b/services/users.js
index fbaf89609..ef3cf7a34 100644
--- a/services/users.js
+++ b/services/users.js
@@ -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
};