Commit 3c23fb4a authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '2-add-user-feedback-to-on-boarding' into 'master'

Add feedback verbiage to guided tour exit routine

Closes #2

See merge request gitlab-org/gitlab-ee!15109
parents b0b482fb 2b508693
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import { glEmojiTag } from '~/emoji';
export const ONBOARDING_DISMISSED_COOKIE_NAME = 'onboarding_dismissed'; export const ONBOARDING_DISMISSED_COOKIE_NAME = 'onboarding_dismissed';
...@@ -31,16 +32,36 @@ export const LABEL_SEARCH_QUERY = `scope=all&state=opened&label_name[]=${encodeU ...@@ -31,16 +32,36 @@ export const LABEL_SEARCH_QUERY = `scope=all&state=opened&label_name[]=${encodeU
ACCEPTING_MR_LABEL_TEXT, ACCEPTING_MR_LABEL_TEXT,
)}`; )}`;
export const EXIT_TOUR_CONTENT = { export const FEEDBACK_CONTENT = {
text: sprintf( text: sprintf(
s__( s__(
'UserOnboardingTour|Thanks for taking the guided tour. Remember, if you want to go through it again, you can start %{emphasisStart}Learn GitLab%{emphasisEnd} in the help menu on the top right.', "UserOnboardingTour|Great job! %{clapHands} We hope the tour was helpful and that you learned how to use GitLab.%{lineBreak}%{lineBreak}We'd love to get your feedback on this tour.%{lineBreak}%{lineBreak}%{emphasisStart}How helpful would you say this guided tour was?%{emphasisEnd}%{lineBreak}%{lineBreak}",
), ),
{ {
emphasisStart: '<strong>', emphasisStart: '<strong>',
emphasisEnd: '</strong>', emphasisEnd: '</strong>',
lineBreak: '<br/>',
clapHands: glEmojiTag('clap'),
}, },
false, false,
), ),
buttons: [{ text: s__('UserOnboardingTour|Got it'), btnClass: 'btn-primary', exitTour: true }], feedbackButtons: true,
feedbackSize: 5,
};
export const EXIT_TOUR_CONTENT = {
text: sprintf(
s__('UserOnboardingTour|Thanks for the feedback! %{thumbsUp}'),
{
thumbsUp: glEmojiTag('thumbsup'),
},
false,
),
buttons: [
{
text: s__("UserOnboardingTour|Close 'Learn GitLab'"),
btnClass: 'btn-primary',
exitTour: true,
},
],
}; };
...@@ -18,6 +18,10 @@ export default { ...@@ -18,6 +18,10 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
feedbackContent: {
type: Object,
required: true,
},
exitTourContent: { exitTourContent: {
type: Object, type: Object,
required: true, required: true,
...@@ -41,6 +45,7 @@ export default { ...@@ -41,6 +45,7 @@ export default {
'tourData', 'tourData',
'lastStepIndex', 'lastStepIndex',
'helpContentIndex', 'helpContentIndex',
'tourFeedback',
'exitTour', 'exitTour',
'dismissed', 'dismissed',
]), ]),
...@@ -53,11 +58,11 @@ export default { ...@@ -53,11 +58,11 @@ export default {
'actionPopover', 'actionPopover',
]), ]),
helpContentData() { helpContentData() {
if (this.showStepContent) { if (!this.showStepContent) return null;
return this.exitTour ? this.exitTourContent : this.helpContent; if (this.exitTour) return this.exitTourContent;
} if (this.tourFeedback) return this.feedbackContent;
return null; return this.helpContent;
}, },
completedSteps() { completedSteps() {
return Math.max(this.lastStepIndex, 0); return Math.max(this.lastStepIndex, 0);
...@@ -73,6 +78,7 @@ export default { ...@@ -73,6 +78,7 @@ export default {
'setHelpContentIndex', 'setHelpContentIndex',
'switchTourPart', 'switchTourPart',
'setExitTour', 'setExitTour',
'setTourFeedback',
'setDismissed', 'setDismissed',
]), ]),
init() { init() {
...@@ -111,6 +117,7 @@ export default { ...@@ -111,6 +117,7 @@ export default {
}, },
handleRestartStep() { handleRestartStep() {
this.showExitTourContent(false); this.showExitTourContent(false);
this.handleFeedbackTourContent(false);
Tracking.event(TRACKING_CATEGORY, 'click_link', { Tracking.event(TRACKING_CATEGORY, 'click_link', {
label: this.getTrackingLabel(), label: this.getTrackingLabel(),
property: 'restart_this_step', property: 'restart_this_step',
...@@ -131,19 +138,41 @@ export default { ...@@ -131,19 +138,41 @@ export default {
} }
}, },
handleClickPopoverButton(button) { handleClickPopoverButton(button) {
const { showExitTourContent, exitTour, redirectPath, nextPart, dismissPopover } = button; const {
showExitTourContent,
exitTour,
redirectPath,
nextPart,
dismissPopover,
feedbackResult,
showFeedbackTourContent,
} = button;
const helpContentItems = this.stepContent const helpContentItems = this.stepContent
? this.stepContent.getHelpContent({ projectName: this.projectName }) ? this.stepContent.getHelpContent({ projectName: this.projectName })
: null; : null;
const showNextContentItem = const showNextContentItem =
helpContentItems && helpContentItems &&
helpContentItems.length > 1 && helpContentItems.length > 1 &&
this.helpContentIndex < helpContentItems.length - 1; this.helpContentIndex < helpContentItems.length - 1;
// track feedback
if (feedbackResult) {
Tracking.event(TRACKING_CATEGORY, 'click_link', {
label: 'feedback',
property: 'feedback_result',
value: feedbackResult,
});
}
// display feedback content after user hits the exit button
if (showFeedbackTourContent) {
this.handleFeedbackTourContent(true);
return;
}
// display exit tour content // display exit tour content
if (showExitTourContent) { if (showExitTourContent) {
this.showExitTourContent(true); this.handleShowExitTourContent(true);
return; return;
} }
...@@ -191,6 +220,11 @@ export default { ...@@ -191,6 +220,11 @@ export default {
}); });
this.showExitTourContent(showExitTour); this.showExitTourContent(showExitTour);
}, },
handleFeedbackTourContent(showTourFeedback) {
this.dismissPopover = false;
this.showStepContent = true;
this.setTourFeedback(showTourFeedback);
},
showExitTourContent(showExitTour) { showExitTourContent(showExitTour) {
this.dismissPopover = false; this.dismissPopover = false;
this.showStepContent = true; this.showStepContent = true;
...@@ -230,6 +264,7 @@ export default { ...@@ -230,6 +264,7 @@ export default {
@clickPopoverButton="handleClickPopoverButton" @clickPopoverButton="handleClickPopoverButton"
@restartStep="handleRestartStep" @restartStep="handleRestartStep"
@skipStep="handleSkipStep" @skipStep="handleSkipStep"
@showFeedbackContent="handleFeedbackTourContent"
@showExitTourContent="handleShowExitTourContent" @showExitTourContent="handleShowExitTourContent"
@exitTour="handleExitTour" @exitTour="handleExitTour"
/> />
......
<script> <script>
import { GlPopover, GlButton } from '@gitlab/ui'; import { GlPopover, GlButton, GlButtonGroup } from '@gitlab/ui';
export default { export default {
name: 'HelpContentPopover', name: 'HelpContentPopover',
components: { components: {
GlPopover, GlPopover,
GlButton, GlButton,
GlButtonGroup,
}, },
props: { props: {
target: { target: {
...@@ -68,6 +69,27 @@ export default { ...@@ -68,6 +69,27 @@ export default {
</span> </span>
</template> </template>
</template> </template>
<template v-if="helpContent.feedbackButtons">
<gl-button-group>
<gl-button
v-for="feedbackValue in helpContent.feedbackSize"
:key="feedbackValue"
@click="
callButtonAction({
feedbackResult: feedbackValue,
showExitTourContent: true,
exitTour: true,
})
"
>
{{ feedbackValue }}
</gl-button>
</gl-button-group>
<div class="pt-1">
<small>{{ __('Not helpful') }}</small>
<small class="ml-4">{{ __('Very helpful') }}</small>
</div>
</template>
</div> </div>
</gl-popover> </gl-popover>
</template> </template>
...@@ -132,8 +132,8 @@ export default { ...@@ -132,8 +132,8 @@ export default {
restartStep() { restartStep() {
this.$emit('restartStep'); this.$emit('restartStep');
}, },
showExitTourContent() { showFeedbackContent() {
this.$emit('showExitTourContent', true); this.$emit('showFeedbackContent', true);
}, },
callButtonAction(button) { callButtonAction(button) {
this.$emit('clickPopoverButton', button); this.$emit('clickPopoverButton', button);
...@@ -208,7 +208,7 @@ export default { ...@@ -208,7 +208,7 @@ export default {
</gl-link> </gl-link>
</li> </li>
<li> <li>
<gl-link class="qa-exit-tour-link d-inline-flex" @click="showExitTourContent"> <gl-link class="qa-exit-tour-link d-inline-flex" @click="showFeedbackContent">
<icon name="leave" class="mr-1" /> <icon name="leave" class="mr-1" />
<span>{{ s__("UserOnboardingTour|Exit 'Learn GitLab'") }}</span> <span>{{ s__("UserOnboardingTour|Exit 'Learn GitLab'") }}</span>
</gl-link> </gl-link>
......
...@@ -3,7 +3,7 @@ import { mapActions } from 'vuex'; ...@@ -3,7 +3,7 @@ import { mapActions } from 'vuex';
import OnboardingApp from './components/app.vue'; import OnboardingApp from './components/app.vue';
import createStore from './store'; import createStore from './store';
import onboardingUtils from './../utils'; import onboardingUtils from './../utils';
import { TOUR_TITLES, EXIT_TOUR_CONTENT } from './../constants'; import { TOUR_TITLES, FEEDBACK_CONTENT, EXIT_TOUR_CONTENT } from './../constants';
import TOUR_PARTS from './../tour_parts'; import TOUR_PARTS from './../tour_parts';
export default function() { export default function() {
...@@ -51,6 +51,7 @@ export default function() { ...@@ -51,6 +51,7 @@ export default function() {
props: { props: {
tourTitles: TOUR_TITLES, tourTitles: TOUR_TITLES,
exitTourContent: EXIT_TOUR_CONTENT, exitTourContent: EXIT_TOUR_CONTENT,
feedbackContent: FEEDBACK_CONTENT,
goldenTanukiSvgPath, goldenTanukiSvgPath,
}, },
}); });
......
...@@ -29,6 +29,10 @@ export const switchTourPart = ({ dispatch }, tourKey) => { ...@@ -29,6 +29,10 @@ export const switchTourPart = ({ dispatch }, tourKey) => {
dispatch('setHelpContentIndex', 0); dispatch('setHelpContentIndex', 0);
}; };
export const setTourFeedback = ({ commit }, tourFeedback) => {
commit(types.SET_FEEDBACK, tourFeedback);
};
export const setExitTour = ({ commit }, exitTour) => { export const setExitTour = ({ commit }, exitTour) => {
commit(types.SET_EXIT_TOUR, exitTour); commit(types.SET_EXIT_TOUR, exitTour);
}; };
......
...@@ -2,5 +2,6 @@ export const SET_INITIAL_DATA = 'SET_INITIAL_DATA'; ...@@ -2,5 +2,6 @@ export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
export const SET_TOUR_KEY = 'SET_TOUR_KEY'; export const SET_TOUR_KEY = 'SET_TOUR_KEY';
export const SET_LAST_STEP_INDEX = 'SET_LAST_STEP_INDEX'; export const SET_LAST_STEP_INDEX = 'SET_LAST_STEP_INDEX';
export const SET_HELP_CONTENT_INDEX = 'SET_HELP_CONTENT_INDEX'; export const SET_HELP_CONTENT_INDEX = 'SET_HELP_CONTENT_INDEX';
export const SET_FEEDBACK = 'SET_FEEDBACK';
export const SET_EXIT_TOUR = 'SET_EXIT_TOUR'; export const SET_EXIT_TOUR = 'SET_EXIT_TOUR';
export const SET_DISMISSED = 'SET_DISMISSED'; export const SET_DISMISSED = 'SET_DISMISSED';
...@@ -13,6 +13,9 @@ export default { ...@@ -13,6 +13,9 @@ export default {
[types.SET_HELP_CONTENT_INDEX](state, payload) { [types.SET_HELP_CONTENT_INDEX](state, payload) {
state.helpContentIndex = payload; state.helpContentIndex = payload;
}, },
[types.SET_FEEDBACK](state, payload) {
state.tourFeedback = payload;
},
[types.SET_EXIT_TOUR](state, payload) { [types.SET_EXIT_TOUR](state, payload) {
state.exitTour = payload; state.exitTour = payload;
}, },
......
...@@ -11,4 +11,5 @@ export default () => ({ ...@@ -11,4 +11,5 @@ export default () => ({
dismissed: false, dismissed: false,
createdProjectPath: '', createdProjectPath: '',
exitTour: false, exitTour: false,
tourFeedback: false,
}); });
...@@ -90,7 +90,7 @@ export default { ...@@ -90,7 +90,7 @@ export default {
<p class="mt-4"> <p class="mt-4">
{{ {{
__( __(
'We created a short guided tour that will help you learn the basics of GitLab and how it will help you be better at your job. It should only take a couple of minutes. You willl be guided by two types of helpers, best recognized by their color.', 'We created a short guided tour that will help you learn the basics of GitLab and how it will help you be better at your job. It should only take a couple of minutes. You will be guided by two types of helpers, best recognized by their color.',
) )
}} }}
</p> </p>
......
---
title: Add user feedback to exit routine of onboarding tour
merge_request:
author:
type: changed
...@@ -20,6 +20,11 @@ describe('User onboarding helper app', () => { ...@@ -20,6 +20,11 @@ describe('User onboarding helper app', () => {
}; };
const tourTitles = [{ id: 1, title: 'First tour' }, { id: 2, title: 'Second tour' }]; const tourTitles = [{ id: 1, title: 'First tour' }, { id: 2, title: 'Second tour' }];
const exitTourContent = { const exitTourContent = {
text: 'feedback content',
feedbackButtons: true,
feedbackSize: 5,
};
const feedbackContent = {
text: 'exit tour content', text: 'exit tour content',
buttons: [{ text: 'OK', btnClass: 'btn-primary' }], buttons: [{ text: 'OK', btnClass: 'btn-primary' }],
}; };
...@@ -27,6 +32,7 @@ describe('User onboarding helper app', () => { ...@@ -27,6 +32,7 @@ describe('User onboarding helper app', () => {
const defaultProps = { const defaultProps = {
tourTitles, tourTitles,
exitTourContent, exitTourContent,
feedbackContent,
goldenTanukiSvgPath: 'illustrations/golden_tanuki.svg', goldenTanukiSvgPath: 'illustrations/golden_tanuki.svg',
}; };
...@@ -70,6 +76,12 @@ describe('User onboarding helper app', () => { ...@@ -70,6 +76,12 @@ describe('User onboarding helper app', () => {
expect(vm.helpContentData).toEqual(exitTourContent); expect(vm.helpContentData).toEqual(exitTourContent);
}); });
it('returns an object containing tour feedback content if tourFeedback is true', () => {
store.dispatch('setTourFeedback', true);
expect(vm.helpContentData).toEqual(feedbackContent);
});
}); });
describe('completedSteps', () => { describe('completedSteps', () => {
...@@ -164,12 +176,14 @@ describe('User onboarding helper app', () => { ...@@ -164,12 +176,14 @@ describe('User onboarding helper app', () => {
}); });
describe('handleRestartStep', () => { describe('handleRestartStep', () => {
it('calls the "showExitTourContent" method', () => { it('calls the "showExitTourContent" and "handleFeedbackTourContent" methods', () => {
spyOn(vm, 'showExitTourContent'); spyOn(vm, 'showExitTourContent');
spyOn(vm, 'handleFeedbackTourContent');
vm.handleRestartStep(); vm.handleRestartStep();
expect(vm.showExitTourContent).toHaveBeenCalledWith(false); expect(vm.showExitTourContent).toHaveBeenCalledWith(false);
expect(vm.handleFeedbackTourContent).toHaveBeenCalledWith(false);
}); });
it('emits the "onboardingHelper.hideActionPopover" event', () => { it('emits the "onboardingHelper.hideActionPopover" event', () => {
...@@ -308,6 +322,22 @@ describe('User onboarding helper app', () => { ...@@ -308,6 +322,22 @@ describe('User onboarding helper app', () => {
}); });
}); });
describe('handleFeedbackTourContent', () => {
it('sets the "dismissPopover" prop to false', () => {
vm.handleFeedbackTourContent(true);
expect(vm.dismissPopover).toBeFalsy();
});
it('calls the "setTourFeedback" method', () => {
spyOn(vm.$store, 'dispatch');
vm.handleFeedbackTourContent(true);
expect(vm.$store.dispatch).toHaveBeenCalledWith('setTourFeedback', true);
});
});
describe('handleExitTour', () => { describe('handleExitTour', () => {
it('calls the "hideActionPopover" method', () => { it('calls the "hideActionPopover" method', () => {
spyOn(vm, 'hideActionPopover'); spyOn(vm, 'hideActionPopover');
......
...@@ -226,7 +226,7 @@ describe('User onboarding tour parts list', () => { ...@@ -226,7 +226,7 @@ describe('User onboarding tour parts list', () => {
it('emits the "showExitTourContent" event when the "Exit Learn GitLab" link is clicked', () => { it('emits the "showExitTourContent" event when the "Exit Learn GitLab" link is clicked', () => {
wrapper.find('.qa-exit-tour-link').vm.$emit('click'); wrapper.find('.qa-exit-tour-link').vm.$emit('click');
expect(wrapper.emitted('showExitTourContent')).toBeTruthy(); expect(wrapper.emitted('showFeedbackContent')).toBeTruthy();
}); });
}); });
......
...@@ -8,6 +8,7 @@ import { ...@@ -8,6 +8,7 @@ import {
setLastStepIndex, setLastStepIndex,
setHelpContentIndex, setHelpContentIndex,
switchTourPart, switchTourPart,
setTourFeedback,
setExitTour, setExitTour,
setDismissed, setDismissed,
} from 'ee/onboarding/onboarding_helper/store/actions'; } from 'ee/onboarding/onboarding_helper/store/actions';
...@@ -140,6 +141,21 @@ describe('User onboarding helper store actions', () => { ...@@ -140,6 +141,21 @@ describe('User onboarding helper store actions', () => {
}); });
}); });
describe('setTourFeedback', () => {
it(`commits ${types.SET_FEEDBACK} mutation`, done => {
const tourFeedback = true;
testAction(
setTourFeedback,
tourFeedback,
state,
[{ type: types.SET_FEEDBACK, payload: tourFeedback }],
[],
done,
);
});
});
describe('setDismissed', () => { describe('setDismissed', () => {
it(`commits ${types.SET_DISMISSED} mutation`, done => { it(`commits ${types.SET_DISMISSED} mutation`, done => {
const dismissed = true; const dismissed = true;
......
...@@ -23,6 +23,7 @@ describe('User onboarding helper store mutations', () => { ...@@ -23,6 +23,7 @@ describe('User onboarding helper store mutations', () => {
dismissed: false, dismissed: false,
createdProjectPath: '', createdProjectPath: '',
exitTour: false, exitTour: false,
tourFeedback: false,
}; };
mutations[types.SET_INITIAL_DATA](state, initialData); mutations[types.SET_INITIAL_DATA](state, initialData);
...@@ -66,6 +67,14 @@ describe('User onboarding helper store mutations', () => { ...@@ -66,6 +67,14 @@ describe('User onboarding helper store mutations', () => {
}); });
}); });
describe('SET_FEEDBACK', () => {
it('sets the tourFeedback property to true', () => {
mutations[types.SET_FEEDBACK](state, true);
expect(state.tourFeedback).toBeTruthy();
});
});
describe('SET_DISMISSED', () => { describe('SET_DISMISSED', () => {
it('sets the dismissed property to true', () => { it('sets the dismissed property to true', () => {
mutations[types.SET_DISMISSED](state, true); mutations[types.SET_DISMISSED](state, true);
......
...@@ -125,7 +125,7 @@ describe('User onboarding welcome page', () => { ...@@ -125,7 +125,7 @@ describe('User onboarding welcome page', () => {
it('displays the welcome text', () => { it('displays the welcome text', () => {
expect(wrapper.text()).toContain( expect(wrapper.text()).toContain(
'We created a short guided tour that will help you learn the basics of GitLab and how it will help you be better at your job. It should only take a couple of minutes. You willl be guided by two types of helpers, best recognized by their color.', 'We created a short guided tour that will help you learn the basics of GitLab and how it will help you be better at your job. It should only take a couple of minutes. You will be guided by two types of helpers, best recognized by their color.',
); );
}); });
......
...@@ -10217,6 +10217,9 @@ msgstr "" ...@@ -10217,6 +10217,9 @@ msgstr ""
msgid "Not found." msgid "Not found."
msgstr "" msgstr ""
msgid "Not helpful"
msgstr ""
msgid "Not now" msgid "Not now"
msgstr "" msgstr ""
...@@ -16682,6 +16685,9 @@ msgstr "" ...@@ -16682,6 +16685,9 @@ msgstr ""
msgid "UserOnboardingTour|Click to open the latest commit to see its details." msgid "UserOnboardingTour|Click to open the latest commit to see its details."
msgstr "" msgstr ""
msgid "UserOnboardingTour|Close 'Learn GitLab'"
msgstr ""
msgid "UserOnboardingTour|Commits are shown in chronological order and can be filtered by the commit message or by the branch." msgid "UserOnboardingTour|Commits are shown in chronological order and can be filtered by the commit message or by the branch."
msgstr "" msgstr ""
...@@ -16694,6 +16700,9 @@ msgstr "" ...@@ -16694,6 +16700,9 @@ msgstr ""
msgid "UserOnboardingTour|Got it" msgid "UserOnboardingTour|Got it"
msgstr "" msgstr ""
msgid "UserOnboardingTour|Great job! %{clapHands} We hope the tour was helpful and that you learned how to use GitLab.%{lineBreak}%{lineBreak}We'd love to get your feedback on this tour.%{lineBreak}%{lineBreak}%{emphasisStart}How helpful would you say this guided tour was?%{emphasisEnd}%{lineBreak}%{lineBreak}"
msgstr ""
msgid "UserOnboardingTour|Guided GitLab Tour" msgid "UserOnboardingTour|Guided GitLab Tour"
msgstr "" msgstr ""
...@@ -16757,7 +16766,7 @@ msgstr "" ...@@ -16757,7 +16766,7 @@ msgstr ""
msgid "UserOnboardingTour|Take a look. Here's a nifty menu for quickly creating issues, merge requests, snippets, projects and groups. Click on it and select \"New project\" from the \"GitLab\" section to get started." msgid "UserOnboardingTour|Take a look. Here's a nifty menu for quickly creating issues, merge requests, snippets, projects and groups. Click on it and select \"New project\" from the \"GitLab\" section to get started."
msgstr "" msgstr ""
msgid "UserOnboardingTour|Thanks for taking the guided tour. Remember, if you want to go through it again, you can start %{emphasisStart}Learn GitLab%{emphasisEnd} in the help menu on the top right." msgid "UserOnboardingTour|Thanks for the feedback! %{thumbsUp}"
msgstr "" msgstr ""
msgid "UserOnboardingTour|That's it for issues. Let'st take a look at %{emphasisStart}Merge Requests%{emphasisEnd}." msgid "UserOnboardingTour|That's it for issues. Let'st take a look at %{emphasisStart}Merge Requests%{emphasisEnd}."
...@@ -16964,6 +16973,9 @@ msgstr "" ...@@ -16964,6 +16973,9 @@ msgstr ""
msgid "Version" msgid "Version"
msgstr "" msgstr ""
msgid "Very helpful"
msgstr ""
msgid "View app" msgid "View app"
msgstr "" msgstr ""
...@@ -17171,7 +17183,7 @@ msgstr "" ...@@ -17171,7 +17183,7 @@ msgstr ""
msgid "We couldn't find any results matching" msgid "We couldn't find any results matching"
msgstr "" msgstr ""
msgid "We created a short guided tour that will help you learn the basics of GitLab and how it will help you be better at your job. It should only take a couple of minutes. You willl be guided by two types of helpers, best recognized by their color." msgid "We created a short guided tour that will help you learn the basics of GitLab and how it will help you be better at your job. It should only take a couple of minutes. You will be guided by two types of helpers, best recognized by their color."
msgstr "" msgstr ""
msgid "We detected potential spam in the %{humanized_resource_name}. Please solve the reCAPTCHA to proceed." msgid "We detected potential spam in the %{humanized_resource_name}. Please solve the reCAPTCHA to proceed."
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment