diff --git a/bin/cli b/bin/cli index 33560c53b..52d67c9af 100755 --- a/bin/cli +++ b/bin/cli @@ -9,13 +9,17 @@ const program = require('./commander'); program .command('serve', 'serve the application') + .command('settings', 'interact with the application settings') .command('assets', 'interact with assets') .command('setup', 'setup the application') .command('jobs', 'work with the job queues') .command('token', 'work with the access tokens') .command('users', 'work with the application auth') .command('migration', 'provides utilities for migrating the database') - .command('plugins', 'provides utilities for interacting with the plugin system') + .command( + 'plugins', + 'provides utilities for interacting with the plugin system' + ) .parse(process.argv); /** @@ -25,7 +29,10 @@ program * labled with the PID written out by the parent process. */ process.once('exit', () => { - if ((program.runningCommand.killed === false) && (program.runningCommand.exitCode === null)) { + if ( + program.runningCommand.killed === false && + program.runningCommand.exitCode === null + ) { program.runningCommand.kill('SIGINT'); } }); diff --git a/bin/cli-settings b/bin/cli-settings new file mode 100755 index 000000000..f8768046c --- /dev/null +++ b/bin/cli-settings @@ -0,0 +1,59 @@ +#!/usr/bin/env node + +const program = require('./commander'); +const inquirer = require('inquirer'); +const mongoose = require('../services/mongoose'); +const SettingsService = require('../services/settings'); +const util = require('./util'); + +// Register the shutdown criteria. +util.onshutdown([() => mongoose.disconnect()]); + +/** + * Change the organization name + */ +async function changeOrgName() { + try { + let settings = await SettingsService.retrieve(); + + let {organizationName} = await inquirer.prompt([ + { + name: 'organizationName', + message: 'Organization Name', + default: settings.organizationName + } + ]); + + if (settings.organizationName !== organizationName) { + settings.organizationName = organizationName; + + await SettingsService.update(settings); + + console.log('Settings were updated.'); + } else { + console.log('No update needed, no change was made.'); + } + } catch (err) { + console.error(err); + util.shutdown(1); + } + + util.shutdown(); +} + +//============================================================================== +// Setting up the program command line arguments. +//============================================================================== + +program + .command('change-org-name') + .description('change the organization name') + .action(changeOrgName); + +program.parse(process.argv); + +// If there is no command listed, output help. +if (!process.argv.slice(2).length) { + program.outputHelp(); + util.shutdown(); +} diff --git a/client/coral-embed-stream/src/actions/stream.js b/client/coral-embed-stream/src/actions/stream.js index 9b5d94a9b..75c665315 100644 --- a/client/coral-embed-stream/src/actions/stream.js +++ b/client/coral-embed-stream/src/actions/stream.js @@ -1,4 +1,4 @@ -import {pym} from 'coral-framework'; +import pym from 'coral-framework/services/pym'; import * as actions from '../constants/stream'; export const setActiveReplyBox = (id) => ({type: actions.SET_ACTIVE_REPLY_BOX, id}); diff --git a/client/coral-embed-stream/src/components/Comment.css b/client/coral-embed-stream/src/components/Comment.css index 4ff0586da..4c8a9fc86 100644 --- a/client/coral-embed-stream/src/components/Comment.css +++ b/client/coral-embed-stream/src/components/Comment.css @@ -1,11 +1,41 @@ -.Reply { - position: relative; +.root { + margin-top: 16px; + margin-left: 20px; margin-bottom: 15px; + position: relative; } -.Comment { - margin-bottom: 15px; - position: relative; +.rootLevel0 { + margin-left: 0px; +} + +.comment { + padding-left: 20px; +} + +.commentLevel0 { + padding-left: 0px; +} + +.commentLevel1 { + border-left: 3px #212121 solid; +} + +.commentLevel2 { + border-left: 2px #6a6a6a solid; +} + +.commentLevel3 { + border-left: 2px #9e9e9e solid; +} + +.commentLevel4 { + border-left: 2px #c1c1c1 solid; +} + +.highlightedComment { + padding-left: 20px; + border-left: 3px solid rgb(35,118,216); } .pendingComment { diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 2e854633b..7cc8b1480 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -9,6 +9,9 @@ import {can} from 'coral-framework/services/perms'; import {TransitionGroup} from 'react-transition-group'; import cn from 'classnames'; import styles from './Comment.css'; +import {THREADING_LEVEL} from '../constants/stream'; +import merge from 'lodash/merge'; +import mapValues from 'lodash/mapValues'; import { BestButton, @@ -316,6 +319,7 @@ export default class Comment extends React.Component { postDontAgree, setActiveReplyBox, activeReplyBox, + loadMore, deleteAction, disableReply, maxCharCount, @@ -331,9 +335,12 @@ export default class Comment extends React.Component { const view = this.getVisibileReplies(); const {loadingState} = this.state; + const isReply = !!parentId; + const isPending = comment.id.indexOf('pending') >= 0; + const isHighlighted = highlighted === comment.id; const hasMoreComments = comment.replies && (comment.replies.hasNextPage || comment.replies.nodes.length > view.length); - const replyCount = this.hasIgnoredReplies() ? '' : comment.replyCount; + const moreRepliesCount = this.hasIgnoredReplies() ? -1 : comment.replyCount - view.length; const flagSummary = getActionSummary('FlagActionSummary', comment); const dontAgreeSummary = getActionSummary( 'DontAgreeActionSummary', @@ -346,11 +353,6 @@ export default class Comment extends React.Component { myFlag = dontAgreeSummary.find((s) => s.current_user); } - let commentClass = parentId - ? `reply ${styles.Reply}` - : `comment ${styles.Comment}`; - commentClass += comment.id.indexOf('pending') >= 0 ? ` ${styles.pendingComment}` : ''; - // call a function, and if it errors, call addNotification('error', ...) (e.g. to show user a snackbar) const notifyOnError = (fn, errorToMessage) => async function(...args) { @@ -386,7 +388,7 @@ export default class Comment extends React.Component { ); /** - * classNamesToAdd + * conditionClassNames * adds classNames based on condition * classnames is an array of objects with key as classnames and value as conditions * i.e: @@ -396,35 +398,45 @@ export default class Comment extends React.Component { * * This will add myClassName to comments tagged with STAFF TAG. * **/ - - const classNamesToAdd = commentClassNames.reduce((acc, className) => { - let res = []; - - // Adding classNames based on tags - Object.keys(className).forEach((cn) => { - const condition = className[cn]; - condition.tags.forEach((tag) => { - if (hasTag(comment.tags, tag)) { - res = [...res, cn]; - } - }); + const conditionalClassNames = + mapValues(merge({}, ...commentClassNames), (condition) => { + if (condition.tags) { + return condition.tags.some((tag) => hasTag(comment.tags, tag)); + } + return false; }); - // TODO: Compare rest of the comment obj to the condition if needed + const rootClassName = cn( + 'talk-stream-comment-wrapper', + `talk-stream-comment-wrapper-level-${depth}`, + styles.root, + styles[`rootLevel${depth}`], + { + ...conditionalClassNames, + [styles.enter]: this.state.animateEnter, + }, + ); - return [...acc, ...res]; - }, []); + const commentClassName = cn( + 'talk-stream-comment', + `talk-stream-comment-level-${depth}`, + styles.comment, + styles[`commentLevel${depth}`], + { + [styles.pendingComment]: isPending, + [styles.highlightedComment]: isHighlighted, + 'talk-stream-pending-comment': isPending, + 'talk-stream-highlighted-comment': isHighlighted, + } + ); return (
-
-
+ {!isReply &&
} +
{isStaff(comment.tags) ? Staff : null} @@ -553,7 +565,7 @@ export default class Comment extends React.Component { charCountEnable={charCountEnable} maxCharCount={maxCharCount} setActiveReplyBox={setActiveReplyBox} - parentId={parentId || comment.id} + parentId={(depth < THREADING_LEVEL) ? comment.id : parentId} addNotification={addNotification} postComment={postComment} currentUser={currentUser} @@ -583,6 +595,7 @@ export default class Comment extends React.Component { deleteAction={deleteAction} addTag={addTag} removeTag={removeTag} + loadMore={loadMore} ignoreUser={ignoreUser} charCountEnable={charCountEnable} maxCharCount={maxCharCount} @@ -598,7 +611,7 @@ export default class Comment extends React.Component {
{ if (!count) { - return t('framework.view_all_replies_unknown_number'); + return t('framework.show_all_replies'); } if (count === 1) { - return t('framework.view_reply'); + return t('framework.show_1_more_reply'); } - if (this.initialState) { - return t('framework.view_all_replies_initial', count); - } else { - return t('framework.view_all_replies', count); - } - } - - componentWillReceiveProps(nextProps) { - if (['success', 'error'].indexOf(nextProps.loadingState) >= 0) { - this.initialState = false; - } + return t('framework.show_x_more_replies', count); } render () { diff --git a/client/coral-embed-stream/src/components/NewCount.js b/client/coral-embed-stream/src/components/NewCount.js index 1c135ab9b..8f3b77dcb 100644 --- a/client/coral-embed-stream/src/components/NewCount.js +++ b/client/coral-embed-stream/src/components/NewCount.js @@ -3,7 +3,7 @@ import React, {PropTypes} from 'react'; import t from 'coral-framework/services/i18n'; const NewCount = ({count, loadMore}) => { - return
+ return
{ count ? { @@ -214,12 +219,3 @@ export default class FlagButton extends Component { ); } } - -const styles = { - flaggedIcon: { - color: '#F00' - }, - unflaggedIcon: { - color: 'inherit' - } -}; diff --git a/client/coral-plugin-flags/components/styles.css b/client/coral-plugin-flags/components/styles.css new file mode 100644 index 000000000..6de9df387 --- /dev/null +++ b/client/coral-plugin-flags/components/styles.css @@ -0,0 +1,7 @@ +.button { + margin: 5px 0px 5px 10px; +} + +.flaggedIcon { + color: #f00 +} diff --git a/client/coral-plugin-replies/ReplyButton.css b/client/coral-plugin-replies/ReplyButton.css new file mode 100644 index 000000000..4f71e6630 --- /dev/null +++ b/client/coral-plugin-replies/ReplyButton.css @@ -0,0 +1,5 @@ +@media (max-width: 425px) { + .label { + display: none; + } +} diff --git a/client/coral-plugin-replies/ReplyButton.js b/client/coral-plugin-replies/ReplyButton.js index a66e3c156..4641ae02d 100644 --- a/client/coral-plugin-replies/ReplyButton.js +++ b/client/coral-plugin-replies/ReplyButton.js @@ -2,16 +2,19 @@ import React, {PropTypes} from 'react'; import t from 'coral-framework/services/i18n'; -import classnames from 'classnames'; +import cn from 'classnames'; +import styles from './ReplyButton.css'; const name = 'coral-plugin-replies'; const ReplyButton = ({onClick}) => { return ( diff --git a/client/coral-settings/containers/ProfileContainer.js b/client/coral-settings/containers/ProfileContainer.js index 5e343e72f..a4cb9459f 100644 --- a/client/coral-settings/containers/ProfileContainer.js +++ b/client/coral-settings/containers/ProfileContainer.js @@ -5,7 +5,7 @@ import {bindActionCreators} from 'redux'; import {withStopIgnoringUser} from 'coral-framework/graphql/mutations'; -import {link} from 'coral-framework/services/PymConnection'; +import {link} from 'coral-framework/services/pym'; import NotLoggedIn from '../components/NotLoggedIn'; import IgnoredUsers from '../components/IgnoredUsers'; import {Spinner} from 'coral-ui'; diff --git a/config.js b/config.js index 02f95d070..ce8931235 100644 --- a/config.js +++ b/config.js @@ -29,7 +29,7 @@ const CONFIG = { // JWT_ISSUER is the value for the issuer for the tokens that will be verified // when decoding. If `JWT_ISSUER` is not in the environment, then it will try // `TALK_ROOT_URL`, otherwise, it will be undefined. - JWT_ISSUER: process.env.TALK_JWT_ISSUER || process.env.TALK_ROOT_URL || undefined, + JWT_ISSUER: process.env.TALK_JWT_ISSUER || process.env.TALK_ROOT_URL, // JWT_EXPIRY is the time for which a given token is valid for. JWT_EXPIRY: process.env.TALK_JWT_EXPIRY || '1 day', @@ -91,7 +91,9 @@ const CONFIG = { if (process.env.NODE_ENV === 'test' && !CONFIG.JWT_SECRET) { CONFIG.JWT_SECRET = 'keyboard cat'; } else if (!CONFIG.JWT_SECRET) { - throw new Error('TALK_JWT_SECRET must be provided in the environment to sign/verify tokens'); + throw new Error( + 'TALK_JWT_SECRET must be provided in the environment to sign/verify tokens' + ); } //------------------------------------------------------------------------------ @@ -108,7 +110,7 @@ if (process.env.NODE_ENV === 'test' && !CONFIG.MONGO_URL) { // Reset the redis url in the event it hasn't been overrided and we are in a // testing environment. if (process.env.NODE_ENV === 'test' && !CONFIG.REDIS_URL) { - CONFIG.REDIS_URL = 'redis://localhost'; + CONFIG.REDIS_URL = 'redis://localhost/1'; } //------------------------------------------------------------------------------ @@ -119,10 +121,15 @@ if (process.env.NODE_ENV === 'test' && !CONFIG.REDIS_URL) { * This is true when the recaptcha secret is provided and the Recaptcha feature * is to be enabled. */ -CONFIG.RECAPTCHA_ENABLED = CONFIG.RECAPTCHA_SECRET && CONFIG.RECAPTCHA_SECRET.length > 0 && - CONFIG.RECAPTCHA_PUBLIC && CONFIG.RECAPTCHA_PUBLIC.length > 0; +CONFIG.RECAPTCHA_ENABLED = + CONFIG.RECAPTCHA_SECRET && + CONFIG.RECAPTCHA_SECRET.length > 0 && + CONFIG.RECAPTCHA_PUBLIC && + CONFIG.RECAPTCHA_PUBLIC.length > 0; if (!CONFIG.RECAPTCHA_ENABLED) { - console.warn('Recaptcha is not enabled for login/signup abuse prevention, set TALK_RECAPTCHA_SECRET and TALK_RECAPTCHA_PUBLIC to enable Recaptcha.'); + console.warn( + 'Recaptcha is not enabled for login/signup abuse prevention, set TALK_RECAPTCHA_SECRET and TALK_RECAPTCHA_PUBLIC to enable Recaptcha.' + ); } //------------------------------------------------------------------------------ @@ -138,7 +145,13 @@ if (!CONFIG.RECAPTCHA_ENABLED) { ]; if (requiredProps.some((prop) => !CONFIG[prop])) { - console.warn(`${requiredProps.map((v) => `TALK_${v}`).join(', ')} should be defined in the environment if you would like to send password reset emails from Talk`); + console.warn( + `${requiredProps + .map((v) => `TALK_${v}`) + .join( + ', ' + )} should be defined in the environment if you would like to send password reset emails from Talk` + ); } } diff --git a/locales/en.yml b/locales/en.yml index dce6f621b..44db5e1b8 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -235,9 +235,9 @@ en: success_bio_update: "Your biography has been updated" success_name_update: "Your username has been updated" success_update_settings: "The changes you have made have been applied to the comment stream on this article" - view_all_replies_unknown_number: "view all replies" - view_all_replies: "view {0} replies" - view_all_replies_initial: "view all {0} replies" + show_all_replies: Show all replies + show_1_more_reply: Show 1 more reply + show_x_more_replies: Show {0} more replies view_more_comments: "view more comments" view_reply: "view reply" from_settings_page: "From the Profile Page you can see your comment history." diff --git a/locales/es.yml b/locales/es.yml index 6a0334fcd..a6b1bc4cd 100644 --- a/locales/es.yml +++ b/locales/es.yml @@ -232,9 +232,9 @@ es: success_bio_update: "Tu biografia fue actualizada" success_name_update: "Tu nombre de usuario ha sido actualizado" success_update_settings: "La configuración de este articulo fue actualizada" - view_all_replies_unknown_number: "ver todas las respuestas" - view_all_replies: "ver {0} respuestas" - view_all_replies_initial: "ver todas las {0} respuestas" + show_all_replies: "Mostrar todas las respuestas" + show_1_more_reply: "Mostrar 1 respuesta más" + show_x_more_replies: "Mostrar {0} respuestas más" view_more_comments: "Ver más comentarios" view_reply: "ver respuesta" from_settings_page: "Desde la página de configuración puedes ver tu historia de comentarios." diff --git a/locales/fr.yml b/locales/fr.yml new file mode 100644 index 000000000..77231e17b --- /dev/null +++ b/locales/fr.yml @@ -0,0 +1,355 @@ +fr: + bandialog: + are_you_sure: "Êtes-vous sûr de vouloir bannir {0}?" + ban_user: "Bannir l'utilisateur ?" + banned_user: "Utilisateur banni" + cancel: Annuler + note: "Remarque: bannir cet utilisateur rejettera également ce commentaire." + yes_ban_user: "Oui, bannir cet utilisateur" + bio_offensive: "Cette biographie est offensante" + cancel: Annuler + characters_remaining: "caractères restants" + comment: + anon: Anonyme + ban_user: "Bannir utilisateur" + comment: "Publier un commentaire" + flagged: signalé + view_context: "Afficher le contexte" + comment_box: + post: "Publier" + cancel: "Annuler" + reply: "Répondre" + comment: "Publier un commentaire" + name: "Nom" + comment_post_notif: "Votre commentaire a été publié." + comment_post_notif_premod: "Merci d'avoir envoyé un commentaire. Notre équipe de modération passera en revue votre commentaire sous peu." + comment_post_banned_word: "Votre commentaire contient un ou plusieurs mots qui ne sont pas autorisés, il ne sera pas publié. Si vous pensez qu'il y a une erreur, contactez notre équipe de modération." + characters_remaining: "caractères restants" + comment_is_best: "Ce commentaire est l'un des meilleurs" + comment_offensive: "Ce commentaire est offensant" + comment_plural: Commentaires + comment_post_banned_word: "Votre commentaire contient un ou plusieurs mots qui ne sont pas autorisés, il ne sera pas publié. Si vous pensez qu'il y a une erreur, contactez notre équipe de modération." + comment_post_notif: "Votre commentaire a été publié." + comment_post_notif_premod: "Merci d'avoir envoyé un commentaire. Notre équipe de modération passera en revue votre commentaire sous peu." + community: + account_creation_date: "Date de création du compte" + active: Actif + admin: Administrateur + ads_marketing: "Cela ressemble à du marketing / un message publicitaire" + are_you_sure: "Êtes-vous sûr de vouloir bannir {0} ?" + ban_user: "Bannir l'utilisateur ?" + banned: Banni + banned_user: "Utilisateur banni" + cancel: Signalé + dont_like_username: "Je n'aime pas ce nom d'utilisateur" + flaggedaccounts: "Noms d'utilisateurs signalés" + flags: Signalements + impersonating: "Cet utilisateur se fait passer pour quelqu'un d'autre" + loading: "Chargement des résultats" + moderator: Modérateur + newsroom_role: "Rôle de la salle de presse" + no_flagged_accounts: "La liste des comptes signalés est vide." + no_results: "Aucun utilisateur n'a été trouvé avec ce nom d'utilisateur ou cette adresse de messagerie. Ils se cachent !" + note: "Remarque: bannir cet utilisateur ne lui permettra pas de modifier les commentaires ou de supprimer quoi que ce soit." + offensive: "Ce commentaire est offensant" + other: Autre + people: Gens + role: "Sélectionnez le rôle ..." + select_status: "Sélectionnez l'état ..." + spam_ads: "Spam / Annonces" + staff: "Équipe" + status: Statut + username_and_email: "Nom d'utilisateur et e-mail" + yes_ban_user: "Oui bannir l'utilisateur" + configure: + apply: Appliquer + banned_word_text: "Les commentaires qui contiennent ces mots ou ces expressions (non sensibles à la casse) seront automatiquement supprimés du flux de commentaires. Tapez un mot et appuyez sur Entrée ou Tabulation pour ajouter. En option, envoyez une liste séparée par des virgules." + banned_words_title: "Liste des mots interdits" + close: "Fermer" + close_after: "Fermer les commentaires après" + close_stream: "Fermer le fil" + close_stream_configuration: "Ce fil de commentaires est actuellement fermé. En ouvrant ce fil commentaire, de nouveaux commentaires peuvent être soumis et publiés." + closed_comments_desc: "Écrivez un message à afficher lorsque votre fil de commentaires est fermé et n'accepte plus de nouveaux commentaires." + closed_comments_label: "Écrire un message..." + closed_stream_settings: "Message de fil de commentaires fermé" + comment_count_error: "S'il vous plait, entrez un nombre valide." + comment_count_header: "Limiter la longueur d'un commentaire" + comment_count_text_post: caractères + comment_count_text_pre: "Les commentaires seront limités à" + comment_settings: Paramètres + comment_stream: "Fil de commentaires" + comment_stream_will_close: "Le fil de commentaires va se fermer dans" + community: Communauté + configure: Configurer + copy_and_paste: "Copiez et collez le code ci-dessous dans votre CMS pour intégrer votre boîte de commentaires dans vos articles." + custom_css_url: "URL CSS personnalisée" + custom_css_url_desc: "URL d'une feuille de style CSS qui remplacera les styles par défaut d'intégration des commentaires. Peut être interne ou externe." + dashboard: Tableau de bord + days: Journées + description: "En tant qu'administrateur, vous pouvez personnaliser les paramètres du fil de commentaires pour cet élément." + domain_list_text: "Entrez les domaines que vous souhaitez autoriser pour Talk, par exemple vos environnements locaux de production et de production (ex : localhost: 3000 staging.domain.com domain.com)." + domain_list_title: "Domaines autorisés" + edit_comment_timeframe_heading: "Modifier l'horodatage des commentaires" + edit_comment_timeframe_text_pre: "Les commentateurs auront" + edit_comment_timeframe_text_post: "secondes pour modifier leurs commentaires." + embed_comment_stream: "Intégrer le fil" + enable_premod_links_text: "Les modérateurs doivent approuver tout commentaire contenant un lien avant sa publication." + enable_pre_moderation: "Activer modération a priori" + enable_pre_moderation_text: "Les modérateurs doivent approuver tout commentaire avant qu'il ne soit publié." + enable_premod_links: "Modérer les commentaires contenant des liens" + enable_premod: "Activer la modération a priori" + enable_premod_description: "Les modérateurs doivent approuver tout commentaire avant sa publication." + enable_premod_links_description: "Les modérateurs doivent approuver tout commentaire contenant un lien avant sa publication." + enable_questionbox: "Posez une question aux lecteurs." + enable_questionbox_description: "Cette question apparaîtra en haut de ce fil de commentaires. Demandez aux lecteurs de s'exprimer sur un sujet évoqué dans l'article ou posez les termes de la discussion, etc." + hours: Heures + include_comment_stream: "Inclure la description du fil de commentaires pour les lecteurs" + include_comment_stream_desc: "Écrivez un message à ajouter au haut de votre flux de commentaires. Publiez un sujet de discussion, avec des suggestion sur la tenue des débats, etc." + include_text: "Entrez votre texte ici." + include_question_here: "Écrivez votre question ici." + moderate: Modérer + moderation_settings: "Paramètres de la modération" + open: "Ouvert" + open_stream: "Ouvrir le fil" + open_stream_configuration: "Ce fil de commentaires est actuellement ouvert. En fermant ce fil de commentaires, aucun nouveau commentaire ne pourra être envoyé, les commentaires précédents seront toujours affichés." + require_email_verification: "Exiger une vérification de l'e-mail" + require_email_verification_text: "Les nouveaux utilisateurs doivent vérifier leur adresse e-mail avant de commenter." + save_changes: "Sauvegarder les changements" + shortcuts: Raccourcis + sign_out: "Se Déconnecter" + stories: Histoires + stream_settings: "Paramètres du fil" + suspect_word_title: "Liste des mots suspects" + suspect_word_text: "Les commentaires contenant ces mots ou expressions (non sensibles à la casse) seront mis en évidence dans le flux de commentaires. Tapez un mot et appuyez sur Entrée ou Tabulation pour ajouter. En option, entrez une liste séparée par des virgules." + tech_settings: "Paramètres techniques" + title: "Configurer le fil de commentaires" + weeks: Semaines + wordlist: "Mots interdits" + continue: Continuer + dashboard: + auto_update: "Les données sont automatiquement mises à jour toutes les cinq minutes ou lorsque vous rechargez." + comment_count: commentaires + flags: signalements + most_flags: "Articles avec le plus de signalements" + most_conversations: "Articles avec le plus de conversations" + next_update: "{0} minutes jusqu'à la prochaine mise à jour." + no_activity: "Il n'y a eu aucun commentaire nulle part dans les cinq dernières minutes." + no_flags: "Il n'y a pas eu aucun signalement au cours des 5 dernières minutes! Hooray !" + no_likes: "Il n'y a pas eu de \"J'aime\" au cours des 5 dernières minutes. Tout est calme." + done: Terminé + edit_comment: + body_input_label: "Modifier ce commentaire" + save_button: "Sauvegarder les changements" + edit_window_expired: "Vous ne pouvez plus modifier ce commentaire. La fenêtre de temps pour le faire a expiré. Pourquoi ne pas en publier un autre ?" + edit_window_expired_close: "Fermer" + edit_window_timer_prefix: "Fenêtre d'édition :" + second: "seconde" + seconds_plural: "secondes" + unexpected_error: "Erreur inattendue lors de l'enregistrement des modifications. Désolé !" + embedlink: + copy: "Copier dans le presse-papier" + error: + COMMENT_TOO_SHORT: "Votre commentaire doit contenir quelque chose" + NOT_AUTHORIZED: "Vous n'êtes pas autorisé à effectuer cette action." + NO_SPECIAL_CHARACTERS: "Les noms d'utilisateur ne peuvent contenir que des lettres, des chiffres et \"_\" seulement" + PASSWORD_LENGTH: "Le mot de passe est trop court" + PROFANITY_ERROR: "Les noms d'utilisateur ne doivent pas contenir de mots offensants. Veuillez contacter l'administrateur si vous pensez qu'il y a une erreur." + USERNAME_IN_USE: "Ce nom d'utilisateur est déjà pris" + USERNAME_REQUIRED: "Doit entrer un nom d'utilisateur" + EDIT_WINDOW_ENDED: "Vous ne pouvez plus modifier ce commentaire. La fenêtre de temps pour le faire a expiré." + EDIT_USERNAME_NOT_AUTHORIZED: "Vous n'avez pas la permission de mettre à jour votre nom d'utilisateur." + EMAIL_IN_USE: "Adresse e-mail déjà utilisée" + EMAIL_REQUIRED: "Une adresse email est requise" + LOGIN_MAXIMUM_EXCEEDED: "Vous avez effectué trop de tentatives infructueuses pour entrer votre mot de passe. S'il vous plaît, attendez." + PASSWORD_REQUIRED: "Doit entrer un mot de passe" + COMMENTING_CLOSED: "Les commentaires sont déjà fermés" + NOT_FOUND: "Ressource introuvable" + INVALID_ASSET_URL: "L'URL est invalide" + email: "Pas une adresse e-mail valide" + confirm_password: "Les mots de passe ne correspondent pas. Vérifiez à nouveau" + network_error: "Échec de connexion au serveur. Vérifiez votre connexion Internet et réessayez." + email_not_verified: "L'adresse e-mail {0} n'est pas vérifiée." + email_password: "Combinaison e-mail / mot de passe incorrecte." + organization_name: "Le nom de l'organisation ne peut contenir que des lettres ou des chiffres." + password: "Le mot de passe doit être d'au moins 8 caractères" + username: "Les noms d'utilisateur ne peuvent contenir que des chiffres, des lettres et \"_\"" + flag_comment: "Signaler un commentaire" + flag_reason: "Motif du signalement (facultatif)" + flag_username: "Signaler un nom d'utilisateur" + framework: + banned_account_msg: "Votre compte est actuellement suspendu. Cela signifie que vous ne pouvez pas Aimer Signaler ou Écrire des commentaires. Contactez-nous si vous avez des questions." + comment: commentaire + comment_is_ignored: "Ce commentaire est caché car vous avez ignoré cet utilisateur." + comments: commentaires + configure_stream: "Configure le fil" + content_not_available: "Ce contenu n'est pas disponible" + edit_name: + button: Soumettre + error: "Les noms d'utilisateur ne peuvent contenir que des chiffres, des lettres et \"_\"" + label: "Nouveau nom d'utilisateur" + msg: "Votre compte est actuellement suspendu car votre nom d'utilisateur a été jugé inapproprié. Pour restaurer votre compte, entrez un nouveau nom d'utilisateur. Contactez-nous si vous avez des questions." + ignored_users: "Utilisateurs ignorés" + my_comments: "Mes commentaires" + my_profile: "Mon profil" + new_count: "Voir {0} nouveau {1}" + profile: Profil + show_all_comments: "Afficher tous les commentaires" + success_bio_update: "Votre biographie a été mise à jour" + success_name_update: "Votre nom d'utilisateur a été mis à jour" + success_update_settings: "Les modifications que vous avez apportées ont été appliqué es au fil de commentaires sur cet article" + view_all_replies: "Voir {0} réponses" + view_all_replies_initial: "Voir toutes les {0} réponses" + view_more_comments: "Voir plus de commentaires" + view_reply: "Voir la réponse" + from_settings_page: "Dans la page Profil, vous pouvez voir l'historique de vos commentaires." + like: "J'aime" + loading_results: "Chargement des résultats" + marketing: "Cela ressemble à du marketing / une publicité" + moderate_this_stream: "Modifiez ce fil" + modqueue: + account: "Signalements du compte" + actions: Actions + all: tous + all_streams: "Tous les fils" + approve: "Approuver" + approved: "Approuvé" + ban_user: "Bannir" + billion: B + close: Fermer + dont_like_username: "N'aime pas le nom d'utilisateur" + empty_queue: "Plus de commentaires à modérer ! Vous avez terminé, allez prendre un ☕️" + flagged: signalé + impersonating: "problème d'identité" + less_detail: "Moins de détails" + likes: "J'aime" + million: M + mod_faster: "Modérer plus rapidement avec les raccourcis clavier" + moderate: "Modérer →" + more_detail: "Plus de détails" + newest_first: "Le plus récent d'abord" + navigation: Navigation + next_comment: "Aller au prochain commentaire" + offensive: Offensant + oldest_first: "Le plus ancien d'abord" + other: Autre + premod: Pre-modérer + prev_comment: "Aller au commentaire précédent" + reject: "Rejeter" + rejected: "Rejeté" + select_stream: "Sélectionnez le fil" + shift_key: ⇧ + shortcuts: Raccourcis + show_shortcuts: "Afficher les raccourcis" + singleview: "Passer en mode d'édition de commentaire unique" + spam_ads: Spam / Publicités + thismenu: "Ouvrir ce menu" + thousand: k + try_these: "Essayez ces" + view_more_shortcuts: "Afficher plus de raccourcis" + my_comment_history: "Mon historique de commentaires" + name: Nom + no_agree_comment: "Je ne suis pas d'accord avec ce commentaire" + no_like_bio: "Je n'aime pas cette biographie" + no_like_username: "Je n'aime pas ce nom d'utilisateur" + other: Autre + permalink: Lien + personal_info: "Ce commentaire révèle des informations personnelles identifiables" + post: Publier + profile: Profil + profile_settings: "Paramètres du profil" + reply: Répondre + report: Signaler + report_notif: "Merci de signaler ce commentaire. Notre équipe de modération a é té informée." + report_notif_remove: "Votre signalement a été supprimé." + reported: Signalé + set_best: "Sélectionner comme le meilleur" + settings: + all_comments: "Tous les commentaires" + from_settings_page: "Dans la page Profil, vous pouvez voir l'historique de vos commentaires." + my_comment_history: "Mon historique de commentaires" + profile: Profil + profile_settings: "Paramètres du profil" + sign_in: "Se connecter" + to_access: "Accéder au profil" + user_no_comment: "Vous n'avez jamais laissé de commentaire. Rejoignez la conversation!" + stream: + temporarily_suspended: "Conformément à la charte d'utilisation des commentaires de {0}, votre compte a été temporairement suspendu. Merci de revenir dans la conversation {1}." + step_1_header: "Signaler un problème" + step_2_header: "Aidez-nous à comprendre" + step_3_header: "Merci pour votre participation" + streams: + all: Tous + article: Récit + closed: Fermé + empty_result: "Aucun contenu ne correspond à cette recherche. Peut-être essayer d'élargir votre recherche ?" + filter_streams: "Filtrer les flux" + newest: Le plus récent + oldest: Le plus ancien + open: Ouvrir + pubdate: "Date de publication" + search: Recherche + sort_by: "Trier par" + status: "Statut du fil" + stream_status: "Statut du fil" + suspenduser: + bio: Bio + cancel: "Annuler" + days: "{0} jours" + description_0: "Voulez-vous interdire temporairement cet utilisateur en raison de leur {0} ? Cela dissimulera temporairement leurs commentaires jusqu'à ce qu'ils réécrivent leurs {0}." + description_1: "Suspendre cet utilisateur désactivera temporairement son compte et cachera tous ses commentaires sur le site." + description_notify: "Suspendre cet utilisateur désactivera temporairement son compte et cachera tous ses commentaires sur le site." + description_reject: "Voulez-vous interdire temporairement cet utilisateur en raison de leur {0}? Cela dissimulera temporairement leurs commentaires jusqu'à ce qu'ils réécrivent leurs {0}." + description_suspend: "Vous suspendez {0}. Ce commentaire sera rejeté, et {0} ne sera pas autorisé à aimer, à signaler, à répondre ou à publier jusqu'à ce que la suspension soit levée." + email: "Un autre membre de la communauté a récemment signalé votre nom d'utilisateur pour examen. En raison de son contenu, votre utilisateur a été rejeté. Cela signifie que vous ne pouvez plus commenter ou signaler du contenu jusqu'à ce que vous changiez votre nom d'utilisateur. Veuillez nous envoyer un e-mail si vous avez des questions ou des préoccupations." + email_subject: "Votre compte a été suspendu" + email_message_reject: "Un autre membre de la communauté a récemment signalé votre nom d'utilisateur pour examen. En raison de son contenu, votre nom d'utilisateur a été rejeté. Cela signifie que vous ne pouvez plus commenter, ni signaler du contenu jusqu'à ce que vous changiez votre nom d'utilisateur. Veuillez nous envoyer un e-mail si vous avez des questions ou des préoccupations." + email_message_suspend: "Cher {0},\n\nConformément à la charte des commentaires de {1}, votre compte a été temporairement suspendu. Pendant cette période, vous ne pourrez pas commenter, signaler ou participer à d'autres commentaires. \n\nMerci de revenir dans la conversation {2}." + error_email_message_empty: "Vous devez spécifier un message dans l'e-mail." + hours: "{0} heures" + no_cancel: "Pas d'annulation" + notify_suspend_until: "L'utilisateur {0} a été temporairement suspendu. Cette suspension se terminera automatiquement {1}." + one_hour: "1 heure" + send: Envoyer + select_duration: "Sélectionnez la durée de la suspension" + suspend_user: "Suspendre l'utilisateur" + title: "Suspendre un utilisateur" + title_0: "Nous avons remarqué que vous avez rejeté un nom d'utilisateur" + title_1: "Aviser l'utilisateur de sa suspension temporaire" + title_notify: "Aviser l'utilisateur de sa suspension temporaire" + title_reject: "Nous avons remarqué que vous avez rejeté un nom d'utilisateur" + title_suspend: "Suspendre l'utilisateur" + username: "Nom d'utilisateur" + write_message: "Écrire un message" + yes_suspend: "Oui suspendre" + thank_you: "Nous apprécions vos commentaires. Un modérateur passera en revue votre signalement. Merci pour votre aide." + unset_best: "Déselectionner comme le meilleur" + user: + bio_flags: "Signaler pour cette biographie" + user_bio: "Bio de l'utilisateur" + username_flags: "Signaler pour ce nom d'utilisateur" + user_impersonating: "Cet utilisateur se fait passer pour quelqu'un d'autre" + user_no_comment: "Vous n'avez jamais laissé de commentaire. Rejoignez la conversation !" + username_offensive: "Ce nom d'utilisateur est offensant" + view_conversation: "Afficher la conversation" + install: + initial: + description: "Configurez votre communauté Talk en quelques petites étapes." + submit: "Commencer" + add_organization: + description: "Veuillez nous indiquer le nom de votre organisation. Cela apparaîtra dans les e-mails lors de l'invitation de nouveaux membres dans l'équipe." + label: "Nom de l'organisation" + save: "Sauvegarder" + create: + email: "Adresse e-mail" + username: "Nom d'utilisateur" + password: "Mot de passe" + confirm_password: "Confirmez Le mot de passe" + save: "Sauvegarder" + permitted_domains: + title: "Domaines autorisés" + description: "Entrez les domaines que vous souhaitez autoriser pour Talk, par ex. vos environnements locaux, de déploiement et de production (ex localhost:3000, staging.domain.com, domain.com)." + submit: "Terminer l'installation" + final: + description: "Merci d'avoir installé Talk ! Nous avons envoyé un e-mail pour vérifier votre adresse électronique. Pendant que vous terminez la configuration, vous pouvez commencer à engager vos lecteurs dès maintenant." + launch: "Lancer Talk" + close: "Fermez cet installateur" diff --git a/package.json b/package.json index a0040783a..0fe387185 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "talk", - "version": "2.2.0", + "version": "2.3.0", "description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net", "main": "app.js", "scripts": { diff --git a/plugins/coral-plugin-auth/client/translations.yml b/plugins/coral-plugin-auth/client/translations.yml index bc7f06759..189c3d9da 100644 --- a/plugins/coral-plugin-auth/client/translations.yml +++ b/plugins/coral-plugin-auth/client/translations.yml @@ -89,3 +89,47 @@ es: username: Nombre write_your_username: "Edita tu nombre" your_username: "Tu nombre aparece en cada comentario que publiques." +fr: + sign_in: + email_verify_cta: "Merci de vérifier votre adresse e-mail." + request_new_verify_email: "Demander un nouvel envoi d'e-mail :" + verify_email: "Merci d'avoir créé un compte ! Nous avons envoyé un courrier électronique à l'adresse que vous avez fournie pour vérifier votre adresse e-mail." + verify_email2: "Vous devez vérifier votre adresse e-mail avant de vous engager auprès de la communauté." + not_you: "Pas vous ?" + logged_in_as: "Connecté en tant que" + facebook_sign_in: "Connectez-vous avec Facebook" + facebook_sign_up: "Inscrivez-vous avec Facebook" + logout: "Se déconnecter" + sign_in: "Se connecter" + sign_in_to_join: "Connectez-vous pour participer à la conversation" + or: "Ou" + email: "Adresse e-mail" + password: "Mot de passe" + forgot_your_pass: "Mot de passe oublié ?" + need_an_account: "Besoin d'un compte ?" + register: "Inscription" + sign_up: "S'inscrire" + confirm_password: "Confirmez Le mot de passe" + username: "Nom d'utilisateur" + already_have_an_account: "Vous avez déjà un compte ?" + recover_password: "Récupérer le mot de passe" + email_in_use: "L'adresse e-mail est déjà utilisée" + email_or_username_in_use: "Adresse e-mail ou nom d'utilisateur déjà utilisé" + required_field: "Ce champ est obligatoire" + passwords_dont_match: "Les mots de passe ne correspondent pas." + special_characters: "Les noms d'utilisateur ne peuvent contenir que des lettres, des chiffres et \"_\" seulement" + sign_in_to_comment: "Connectez-vous pour commenter" + check_the_form: "Formulaire non valide. Veuillez vérifier les champs" + createdisplay: + check_the_form: "Formulaire non valide. Veuillez vérifier les champs" + continue: "Continuez avec le même nom d'utilisateur que sur Facebook" + error_create: "Erreur lors de la modification du nom d'utilisateur" + fake_comment_body: "Ceci est un exemple de commentaire. Les lecteurs peuvent partager leurs pensées et opinions avec les rédactions dans la section des commentaires." + fake_comment_date: "Il y a 1 minute" + if_you_dont_change_your_name: "Si vous ne modifiez pas votre nom d'utilisateur à cette étape, votre nom d'affichage Facebook apparaîtra à côté de tous vos commentaires." + required_field: "champs requis" + save: Sauvegarder + special_characters: "Les noms d'utilisateur ne peuvent contenir que des chiffres, des lettres et \"_\"" + username: "Nom d'utilisateur" + write_your_username: "Modifier votre nom d'utilisateur" + your_username: "Votre nom d'utilisateur apparaît sur chaque commentaire que vous publiez." diff --git a/plugins/coral-plugin-like/client/LikeButton.js b/plugins/coral-plugin-like/client/LikeButton.js index 7d17ef056..34b5e40f2 100644 --- a/plugins/coral-plugin-like/client/LikeButton.js +++ b/plugins/coral-plugin-like/client/LikeButton.js @@ -45,7 +45,9 @@ class LikeButton extends React.Component { className={cn(styles.button, {[styles.liked]: alreadyReacted}, `${plugin}-button`)} onClick={this.handleClick} > - {t(alreadyReacted ? 'coral-plugin-like.liked' : 'coral-plugin-like.like')} + + {t(alreadyReacted ? 'coral-plugin-like.liked' : 'coral-plugin-like.like')} + {count > 0 && count} diff --git a/plugins/coral-plugin-like/client/styles.css b/plugins/coral-plugin-like/client/styles.css index cb372fa47..b44c170e3 100644 --- a/plugins/coral-plugin-like/client/styles.css +++ b/plugins/coral-plugin-like/client/styles.css @@ -23,3 +23,9 @@ } } } + +@media (max-width: 425px) { + .label { + display: none; + } +} diff --git a/plugins/coral-plugin-love/client/LoveButton.js b/plugins/coral-plugin-love/client/LoveButton.js index 7450ad668..d67160773 100644 --- a/plugins/coral-plugin-love/client/LoveButton.js +++ b/plugins/coral-plugin-love/client/LoveButton.js @@ -45,7 +45,9 @@ class LoveButton extends React.Component { className={cn(styles.button, {[styles.loved]: alreadyReacted}, `${plugin}-button`)} onClick={this.handleClick} > - {t(alreadyReacted ? 'coral-plugin-love.loved' : 'coral-plugin-love.love')} + + {t(alreadyReacted ? 'coral-plugin-love.loved' : 'coral-plugin-love.love')} + {count > 0 && count} diff --git a/plugins/coral-plugin-love/client/styles.css b/plugins/coral-plugin-love/client/styles.css index e16e17ca4..ac5a86370 100644 --- a/plugins/coral-plugin-love/client/styles.css +++ b/plugins/coral-plugin-love/client/styles.css @@ -23,3 +23,9 @@ } } } + +@media (max-width: 425px) { + .label { + display: none; + } +} diff --git a/plugins/coral-plugin-respect/client/RespectButton.js b/plugins/coral-plugin-respect/client/RespectButton.js index 9bc556b67..878525051 100644 --- a/plugins/coral-plugin-respect/client/RespectButton.js +++ b/plugins/coral-plugin-respect/client/RespectButton.js @@ -45,11 +45,11 @@ class RespectButton extends React.Component { className={cn(styles.button, {[styles.respected]: alreadyReacted}, `${plugin}-button`)} onClick={this.handleClick} > - + {t(alreadyReacted ? 'coral-plugin-respect.respected' : 'coral-plugin-respect.respect')} - {count > 0 && count} + {count > 0 && count}
); diff --git a/plugins/coral-plugin-respect/client/styles.css b/plugins/coral-plugin-respect/client/styles.css index f0b780b86..d08d648c3 100644 --- a/plugins/coral-plugin-respect/client/styles.css +++ b/plugins/coral-plugin-respect/client/styles.css @@ -28,3 +28,9 @@ .icon { padding: 0 5px; } + +@media (max-width: 425px) { + .label { + display: none; + } +} diff --git a/plugins/talk-plugin-permalink/client/components/PermalinkButton.js b/plugins/talk-plugin-permalink/client/components/PermalinkButton.js index fc8561bdc..55b98e5d8 100644 --- a/plugins/talk-plugin-permalink/client/components/PermalinkButton.js +++ b/plugins/talk-plugin-permalink/client/components/PermalinkButton.js @@ -66,11 +66,11 @@ export default class PermalinkButton extends React.Component { return (
- + @@ -89,7 +89,7 @@ export default class PermalinkButton extends React.Component {
diff --git a/plugins/talk-plugin-permalink/client/components/styles.css b/plugins/talk-plugin-permalink/client/components/styles.css index 56d069755..f14e87881 100644 --- a/plugins/talk-plugin-permalink/client/components/styles.css +++ b/plugins/talk-plugin-permalink/client/components/styles.css @@ -42,7 +42,13 @@ } .button { + margin: 5px 0px 5px 10px; + cursor: pointer; +} + +.copyButton { display: inline-block; + margin: 5px 10px 5px 0px; float: right; box-sizing: border-box; margin: 0; @@ -52,18 +58,19 @@ height: auto; padding: 2px; transition: background-color 0.4s ease; + cursor: pointer; } -.button:hover{ +.copyButton:hover{ color: black; } -.button.success { +.copyButton.success { background-color: #00897B; color: white; } -.button.failure { +.copyButton.failure { background-color: #FF5252; color: white; } @@ -74,4 +81,4 @@ .active { display: block; -} \ No newline at end of file +} diff --git a/services/i18n.js b/services/i18n.js index eff827cac..a8a160115 100644 --- a/services/i18n.js +++ b/services/i18n.js @@ -2,17 +2,19 @@ const has = require('lodash/has'); const get = require('lodash/get'); const yaml = require('yamljs'); + const es = yaml.load('./locales/es.yml'); const en = yaml.load('./locales/en.yml'); +const fr = yaml.load('./locales/fr.yml'); const accepts = require('accepts'); // default language let defaultLanguage = 'en'; let language = defaultLanguage; -const languages = ['en', 'es']; +const languages = ['en', 'es', 'fr']; -const translations = Object.assign(en, es); +const translations = Object.assign(en, es, fr); /** * Exposes a service object to allow translations. diff --git a/test/helpers/redis.js b/test/helpers/redis.js new file mode 100644 index 000000000..02ab81b51 --- /dev/null +++ b/test/helpers/redis.js @@ -0,0 +1,15 @@ +const {createClient} = require('../../services/redis'); + +// Create a redis client to use for clearing the database. +const client = createClient(); + +module.exports.clearDB = () => + new Promise((resolve, reject) => + client.flushdb((err) => { + if (err) { + return reject(err); + } + + return resolve(); + }) + ); diff --git a/test/server/redis.js b/test/server/redis.js new file mode 100644 index 000000000..529548918 --- /dev/null +++ b/test/server/redis.js @@ -0,0 +1,3 @@ +const redis = require('../helpers/redis'); + +beforeEach(() => redis.clearDB()); diff --git a/webpack.config.js b/webpack.config.js index 130ced27b..c39726882 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -112,7 +112,8 @@ const config = { } }), new webpack.EnvironmentPlugin({ - 'TALK_PLUGINS_JSON': '{}' + 'TALK_PLUGINS_JSON': '{}', + 'TALK_THREADING_LEVEL': '3' }) ], resolveLoader: {