Added translation wrapper to public-facing strings in Portal

refs https://github.com/TryGhost/Ghost/issues/15502

- in order to use the translations, strings must be wrapped in the `t`
  function, which is passed through AppContext
- whilst I haven't instrumented all public strings, the vast majority
  are done here and the strings have been brought into the JSON locale files using `yarn translate:generate`
This commit is contained in:
Daniel Lockyer 2023-02-27 10:39:38 +01:00 committed by Daniel Lockyer
parent f007094d4b
commit acf2ab2d22
19 changed files with 259 additions and 114 deletions

View File

@ -1,3 +1,62 @@
{ {
"Hello": "Hello" "{{discount}}% discount": "",
"{{trialDays}} days free": "",
"A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "",
"Account": "",
"Account settings": "",
"After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "",
"Already a member?": "",
"Back": "",
"Back to Log in": "",
"Cancel subscription": "",
"Cancellation reason": "",
"Choose a different plan": "",
"Choose your newsletters": "",
"Close": "",
"Comments": "",
"Confirm": "",
"Continue": "",
"Delete account": "",
"Don't have an account?": "",
"Email": "",
"Email preference updated.": "",
"Email preferences": "",
"Emails": "",
"Emails disabled": "",
"Get help": "",
"Get notified when someone replies to your comment": "",
"Give feedback on this post": "",
"Hello": "",
"Less like this": "",
"Manage": "",
"Monthly": "",
"More like this": "",
"Name": "",
"Not receiving emails?": "",
"Now check your email!": "",
"Powered by Ghost": "",
"Price": "",
"Re-enable emails": "",
"Retry": "",
"Save": "",
"Sending login link...": "",
"Sending...": "",
"Sign in": "",
"Sign up": "",
"Start {{amount}}-day free trial": "",
"Submit feedback": "",
"Successfully unsubscribed": "",
"Thanks for the feedback!": "",
"That didn't go to plan": "",
"This site is invite-only, contact the owner for access.": "",
"To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "",
"Unsubscribe from all emails": "",
"Unsubscribing from emails will not cancel your paid subscription to {{title}}": "",
"Update your preferences": "",
"We couldn't unsubscribe you as the email address was not found. Please contact the site owner.": "",
"Yearly": "",
"You have been successfully resubscribed": "",
"You're not receiving emails because you either marked a recent message as spam, or because messages could not be delivered to your provided email address.": "",
"Your account": "",
"Your input helps shape what gets published.": ""
} }

View File

@ -1,3 +1,62 @@
{ {
"Hello": "Hallo" "{{discount}}% discount": "",
"{{trialDays}} days free": "",
"A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "",
"Account": "",
"Account settings": "",
"After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "",
"Already a member?": "",
"Back": "",
"Back to Log in": "",
"Cancel subscription": "",
"Cancellation reason": "",
"Choose a different plan": "",
"Choose your newsletters": "",
"Close": "",
"Comments": "",
"Confirm": "",
"Continue": "",
"Delete account": "",
"Don't have an account?": "",
"Email": "",
"Email preference updated.": "",
"Email preferences": "",
"Emails": "",
"Emails disabled": "",
"Get help": "",
"Get notified when someone replies to your comment": "",
"Give feedback on this post": "",
"Hello": "Hallo",
"Less like this": "",
"Manage": "",
"Monthly": "",
"More like this": "",
"Name": "",
"Not receiving emails?": "",
"Now check your email!": "",
"Powered by Ghost": "",
"Price": "",
"Re-enable emails": "",
"Retry": "",
"Save": "",
"Sending login link...": "",
"Sending...": "",
"Sign in": "",
"Sign up": "",
"Start {{amount}}-day free trial": "",
"Submit feedback": "",
"Successfully unsubscribed": "",
"Thanks for the feedback!": "",
"That didn't go to plan": "",
"This site is invite-only, contact the owner for access.": "",
"To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "",
"Unsubscribe from all emails": "",
"Unsubscribing from emails will not cancel your paid subscription to {{title}}": "",
"Update your preferences": "",
"We couldn't unsubscribe you as the email address was not found. Please contact the site owner.": "",
"Yearly": "",
"You have been successfully resubscribed": "",
"You're not receiving emails because you either marked a recent message as spam, or because messages could not be delivered to your provided email address.": "",
"Your account": "",
"Your input helps shape what gets published.": ""
} }

View File

@ -10,13 +10,13 @@ import {ReactComponent as CheckmarkIcon} from '../../images/icons/check-circle.s
const React = require('react'); const React = require('react');
function AccountHeader() { function AccountHeader() {
const {brandColor, lastPage, onAction} = useContext(AppContext); const {brandColor, lastPage, onAction, t} = useContext(AppContext);
return ( return (
<header className='gh-portal-detail-header'> <header className='gh-portal-detail-header'>
<BackButton brandColor={brandColor} hidden={!lastPage} onClick={(e) => { <BackButton brandColor={brandColor} hidden={!lastPage} onClick={(e) => {
onAction('back'); onAction('back');
}} /> }} />
<h3 className='gh-portal-main-title'>Email preferences</h3> <h3 className='gh-portal-main-title'>{t('Email preferences')}</h3>
</header> </header>
); );
} }
@ -86,6 +86,7 @@ function NewsletterPrefSection({newsletter, subscribedNewsletters, setSubscribed
} }
function CommentsSection({updateCommentNotifications, isCommentsEnabled, enableCommentNotifications}) { function CommentsSection({updateCommentNotifications, isCommentsEnabled, enableCommentNotifications}) {
const {t} = useContext(AppContext);
const isChecked = !!enableCommentNotifications; const isChecked = !!enableCommentNotifications;
const [showUpdated, setShowUpdated] = useState(false); const [showUpdated, setShowUpdated] = useState(false);
@ -98,8 +99,8 @@ function CommentsSection({updateCommentNotifications, isCommentsEnabled, enableC
return ( return (
<section className='gh-portal-list-toggle-wrapper' data-test-toggle-wrapper> <section className='gh-portal-list-toggle-wrapper' data-test-toggle-wrapper>
<div className='gh-portal-list-detail'> <div className='gh-portal-list-detail'>
<h3>Comments</h3> <h3>{t('Comments')}</h3>
<p>Get notified when someone replies to your comment</p> <p>{t('Get notified when someone replies to your comment')}</p>
</div> </div>
<div style={{display: 'flex', alignItems: 'center'}}> <div style={{display: 'flex', alignItems: 'center'}}>
<SuccessIcon show={showUpdated} checked={isChecked} /> <SuccessIcon show={showUpdated} checked={isChecked} />
@ -133,9 +134,11 @@ function NewsletterPrefs({subscribedNewsletters, setSubscribedNewsletters}) {
} }
function ShowPaidMemberMessage({site, isPaid}) { function ShowPaidMemberMessage({site, isPaid}) {
const {t} = useContext(AppContext);
if (isPaid) { if (isPaid) {
return ( return (
<p style={{textAlign: 'center', marginTop: '12px', marginBottom: '0', color: 'var(--grey6)'}}>Unsubscribing from emails will not cancel your paid subscription to {site?.title}</p> <p style={{textAlign: 'center', marginTop: '12px', marginBottom: '0', color: 'var(--grey6)'}}>{t('Unsubscribing from emails will not cancel your paid subscription to {{title}}', {title: site?.title})}</p>
); );
} }
return null; return null;
@ -151,7 +154,7 @@ export default function NewsletterManagement({
isCommentsEnabled, isCommentsEnabled,
enableCommentNotifications enableCommentNotifications
}) { }) {
const {brandColor, onAction, member, site} = useContext(AppContext); const {brandColor, onAction, member, site, t} = useContext(AppContext);
const isDisabled = !subscribedNewsletters?.length && ((isCommentsEnabled && !enableCommentNotifications) || !isCommentsEnabled); const isDisabled = !subscribedNewsletters?.length && ((isCommentsEnabled && !enableCommentNotifications) || !isCommentsEnabled);
const EmptyNotification = () => { const EmptyNotification = () => {
return null; return null;
@ -192,7 +195,7 @@ export default function NewsletterManagement({
disabled={isDisabled} disabled={isDisabled}
brandColor={brandColor} brandColor={brandColor}
isPrimary={false} isPrimary={false}
label='Unsubscribe from all emails' label={t('Unsubscribe from all emails')}
isDestructive={true} isDestructive={true}
style={{width: '100%'}} style={{width: '100%'}}
dataTestId="unsubscribe-from-all-emails" dataTestId="unsubscribe-from-all-emails"
@ -201,12 +204,12 @@ export default function NewsletterManagement({
</div> </div>
{hasMemberGotEmailSuppression({member}) && !isDisabled && {hasMemberGotEmailSuppression({member}) && !isDisabled &&
<div className="gh-portal-footer-secondary"> <div className="gh-portal-footer-secondary">
<span className="gh-portal-footer-secondary-light">Not receiving emails?</span> <span className="gh-portal-footer-secondary-light">{t('Not receiving emails?')}</span>
<button <button
className="gh-portal-btn-text gh-email-faq-page-button" className="gh-portal-btn-text gh-email-faq-page-button"
onClick={() => onAction('switchPage', {page: 'emailReceivingFAQ'})} onClick={() => onAction('switchPage', {page: 'emailReceivingFAQ'})}
> >
Get help &rarr; {t('Get help')} &rarr;
</button> </button>
</div> </div>
} }

View File

@ -1,14 +1,19 @@
import React from 'react'; import React from 'react';
import AppContext from '../../AppContext';
import {ReactComponent as GhostLogo} from '../../images/ghost-logo-small.svg'; import {ReactComponent as GhostLogo} from '../../images/ghost-logo-small.svg';
export default class PoweredBy extends React.Component { export default class PoweredBy extends React.Component {
static contextType = AppContext;
render() { render() {
const {t} = this.context;
return ( return (
<a href='https://ghost.org' target='_blank' rel='noopener noreferrer' onClick={() => { <a href='https://ghost.org' target='_blank' rel='noopener noreferrer' onClick={() => {
window.open('https://ghost.org', '_blank'); window.open('https://ghost.org', '_blank');
}}> }}>
<GhostLogo /> <GhostLogo />
Powered by Ghost {t('Powered by Ghost')}
</a> </a>
); );
} }

View File

@ -547,12 +547,12 @@ function ProductCardAlternatePrice({price}) {
} }
function ProductCardTrialDays({trialDays, discount, selectedInterval}) { function ProductCardTrialDays({trialDays, discount, selectedInterval}) {
const {site} = useContext(AppContext); const {site, t} = useContext(AppContext);
if (hasFreeTrialTier({site})) { if (hasFreeTrialTier({site})) {
if (trialDays) { if (trialDays) {
return ( return (
<span className="gh-portal-discount-label">{trialDays} days free</span> <span className="gh-portal-discount-label">{t('{{trialDays}} days free', {trialDays})}</span>
); );
} else { } else {
return null; return null;
@ -561,7 +561,7 @@ function ProductCardTrialDays({trialDays, discount, selectedInterval}) {
if (selectedInterval === 'year') { if (selectedInterval === 'year') {
return ( return (
<span className="gh-portal-discount-label">{discount}% discount</span> <span className="gh-portal-discount-label">{t('{{discount}}% discount', {discount})}</span>
); );
} }
@ -809,7 +809,7 @@ function YearlyDiscount({discount, trialDays}) {
} }
function ProductPriceSwitch({products, selectedInterval, setSelectedInterval}) { function ProductPriceSwitch({products, selectedInterval, setSelectedInterval}) {
const {site} = useContext(AppContext); const {site, t} = useContext(AppContext);
const {portal_plans: portalPlans} = site; const {portal_plans: portalPlans} = site;
if (!portalPlans.includes('monthly') || !portalPlans.includes('yearly')) { if (!portalPlans.includes('monthly') || !portalPlans.includes('yearly')) {
return null; return null;
@ -825,7 +825,7 @@ function ProductPriceSwitch({products, selectedInterval, setSelectedInterval}) {
setSelectedInterval('month'); setSelectedInterval('month');
}} }}
> >
Monthly {t('Monthly')}
</button> </button>
<button <button
data-test-button='switch-yearly' data-test-button='switch-yearly'
@ -834,7 +834,7 @@ function ProductPriceSwitch({products, selectedInterval, setSelectedInterval}) {
setSelectedInterval('year'); setSelectedInterval('year');
}} }}
> >
Yearly {t('Yearly')}
</button> </button>
</div> </div>
</div> </div>

View File

@ -5,7 +5,7 @@ export default class SiteTitleBackButton extends React.Component {
static contextType = AppContext; static contextType = AppContext;
render() { render() {
// const {site} = this.context; const {t} = this.context;
return ( return (
<> <>
<button <button
@ -17,7 +17,7 @@ export default class SiteTitleBackButton extends React.Component {
this.context.onAction('closePopup'); this.context.onAction('closePopup');
} }
}}> }}>
<span>&larr; </span> Back <span>&larr; </span> {t('Back')}
</button> </button>
</> </>
); );

View File

@ -6,7 +6,7 @@ import NewsletterManagement from '../common/NewsletterManagement';
const React = require('react'); const React = require('react');
export default function AccountEmailPage() { export default function AccountEmailPage() {
const {member, onAction, site} = useContext(AppContext); const {member, onAction, site, t} = useContext(AppContext);
useEffect(() => { useEffect(() => {
if (!member) { if (!member) {
@ -40,7 +40,7 @@ export default function AccountEmailPage() {
setSubscribedNewsletters([]); setSubscribedNewsletters([]);
onAction('showPopupNotification', { onAction('showPopupNotification', {
action: 'updated:success', action: 'updated:success',
message: `Email preference updated.` message: t(`Email preference updated.`)
}); });
const data = {newsletters: []}; const data = {newsletters: []};
if (commentsEnabled) { if (commentsEnabled) {

View File

@ -4,14 +4,14 @@ import {isEmailSuppressed} from 'utils/helpers';
import {ReactComponent as EmailDeliveryFailedIcon} from 'images/icons/email-delivery-failed.svg'; import {ReactComponent as EmailDeliveryFailedIcon} from 'images/icons/email-delivery-failed.svg';
function EmailPreferencesAction() { function EmailPreferencesAction() {
const {onAction, member} = useContext(AppContext); const {onAction, member, t} = useContext(AppContext);
const emailSuppressed = isEmailSuppressed({member}); const emailSuppressed = isEmailSuppressed({member});
const page = emailSuppressed ? 'emailSuppressed' : 'accountEmail'; const page = emailSuppressed ? 'emailSuppressed' : 'accountEmail';
return ( return (
<section> <section>
<div className='gh-portal-list-detail'> <div className='gh-portal-list-detail'>
<h3>Emails</h3> <h3>{t('Emails')}</h3>
{ {
emailSuppressed emailSuppressed
? ( ? (
@ -20,7 +20,7 @@ function EmailPreferencesAction() {
<span>You're <span className="gh-mobile-shortener">currently </span>not receiving emails</span> <span>You're <span className="gh-mobile-shortener">currently </span>not receiving emails</span>
</p> </p>
) )
: <p>Update your preferences</p> : <p>{t('Update your preferences')}</p>
} }
</div> </div>
<button className='gh-portal-btn gh-portal-btn-list' onClick={(e) => { <button className='gh-portal-btn gh-portal-btn-list' onClick={(e) => {
@ -29,7 +29,7 @@ function EmailPreferencesAction() {
lastPage: 'accountHome' lastPage: 'accountHome'
}); });
}} data-test-button='manage-newsletters'> }} data-test-button='manage-newsletters'>
Manage {t('Manage')}
</button> </button>
</section> </section>
); );

View File

@ -3,12 +3,12 @@ import MemberAvatar from 'components/common/MemberGravatar';
import React, {useContext} from 'react'; import React, {useContext} from 'react';
const UserHeader = () => { const UserHeader = () => {
const {member, brandColor} = useContext(AppContext); const {member, brandColor, t} = useContext(AppContext);
const avatar = member.avatar_image; const avatar = member.avatar_image;
return ( return (
<header className='gh-portal-account-header'> <header className='gh-portal-account-header'>
<MemberAvatar gravatar={avatar} style={{userIcon: {color: brandColor, width: '56px', height: '56px', padding: '2px'}}} /> <MemberAvatar gravatar={avatar} style={{userIcon: {color: brandColor, width: '56px', height: '56px', padding: '2px'}}} />
<h2 className="gh-portal-main-title">Your account</h2> <h2 className="gh-portal-main-title">{t('Your account')}</h2>
</header> </header>
); );
}; };

View File

@ -64,7 +64,7 @@ const Header = ({onBack, showConfirmation, confirmationType}) => {
}; };
const CancelSubscriptionButton = ({member, onCancelSubscription, action, brandColor}) => { const CancelSubscriptionButton = ({member, onCancelSubscription, action, brandColor}) => {
const {site} = useContext(AppContext); const {site, t} = useContext(AppContext);
if (!member.paid) { if (!member.paid) {
return null; return null;
} }
@ -77,7 +77,7 @@ const CancelSubscriptionButton = ({member, onCancelSubscription, action, brandCo
if (subscription.cancel_at_period_end) { if (subscription.cancel_at_period_end) {
return null; return null;
} }
const label = 'Cancel subscription'; const label = t('Cancel subscription');
const isRunning = ['cancelSubscription:running'].includes(action); const isRunning = ['cancelSubscription:running'].includes(action);
const disabled = (isRunning) ? true : false; const disabled = (isRunning) ? true : false;
const isPrimary = !!subscription.cancel_at_period_end; const isPrimary = !!subscription.cancel_at_period_end;
@ -110,11 +110,11 @@ const CancelSubscriptionButton = ({member, onCancelSubscription, action, brandCo
// For confirmation flows // For confirmation flows
const PlanConfirmationSection = ({plan, type, onConfirm}) => { const PlanConfirmationSection = ({plan, type, onConfirm}) => {
const {site, action, member, brandColor} = useContext(AppContext); const {site, action, member, brandColor, t} = useContext(AppContext);
const [reason, setReason] = useState(''); const [reason, setReason] = useState('');
const subscription = getMemberSubscription({member}); const subscription = getMemberSubscription({member});
const isRunning = ['updateSubscription:running', 'checkoutPlan:running', 'cancelSubscription:running'].includes(action); const isRunning = ['updateSubscription:running', 'checkoutPlan:running', 'cancelSubscription:running'].includes(action);
const label = 'Confirm'; const label = t('Confirm');
let planStartDate = getDateString(subscription.current_period_end); let planStartDate = getDateString(subscription.current_period_end);
const currentActivePlan = getMemberActivePrice({member}); const currentActivePlan = getMemberActivePrice({member});
if (currentActivePlan.id !== plan.id) { if (currentActivePlan.id !== plan.id) {
@ -123,14 +123,14 @@ const PlanConfirmationSection = ({plan, type, onConfirm}) => {
const priceString = formatNumber(plan.price); const priceString = formatNumber(plan.price);
const planStartMessage = `${plan.currency_symbol}${priceString}/${plan.interval} Starting ${planStartDate}`; const planStartMessage = `${plan.currency_symbol}${priceString}/${plan.interval} Starting ${planStartDate}`;
const product = getProductFromPrice({site, priceId: plan?.id}); const product = getProductFromPrice({site, priceId: plan?.id});
const priceLabel = hasMultipleProductsFeature({site}) ? product?.name : 'Price'; const priceLabel = hasMultipleProductsFeature({site}) ? product?.name : t('Price');
if (type === 'changePlan') { if (type === 'changePlan') {
return ( return (
<div className='gh-portal-logged-out-form-container'> <div className='gh-portal-logged-out-form-container'>
<div className='gh-portal-list mb6'> <div className='gh-portal-list mb6'>
<section> <section>
<div className='gh-portal-list-detail'> <div className='gh-portal-list-detail'>
<h3>Account</h3> <h3>{t('Account')}</h3>
<p>{member.email}</p> <p>{member.email}</p>
</div> </div>
</section> </section>
@ -161,7 +161,7 @@ const PlanConfirmationSection = ({plan, type, onConfirm}) => {
<p>If you cancel your subscription now, you will continue to have access until <strong>{getDateString(subscription.current_period_end)}</strong>.</p> <p>If you cancel your subscription now, you will continue to have access until <strong>{getDateString(subscription.current_period_end)}</strong>.</p>
<section className='gh-portal-input-section'> <section className='gh-portal-input-section'>
<div className='gh-portal-input-labelcontainer'> <div className='gh-portal-input-labelcontainer'>
<label className='gh-portal-input-label'>Cancellation reason</label> <label className='gh-portal-input-label'>{t('Cancellation reason')}</label>
</div> </div>
<textarea <textarea
data-test-input='cancellation-reason' data-test-input='cancellation-reason'

View File

@ -55,10 +55,12 @@ export default class AccountProfilePage extends React.Component {
} }
renderSaveButton() { renderSaveButton() {
const {t} = this.context;
const isRunning = (this.context.action === 'updateProfile:running'); const isRunning = (this.context.action === 'updateProfile:running');
let label = 'Save'; let label = t('Save');
if (this.context.action === 'updateProfile:failed') { if (this.context.action === 'updateProfile:failed') {
label = 'Retry'; label = t('Retry');
} }
const disabled = isRunning ? true : false; const disabled = isRunning ? true : false;
return ( return (
@ -75,8 +77,10 @@ export default class AccountProfilePage extends React.Component {
} }
renderDeleteAccountButton() { renderDeleteAccountButton() {
const {t} = this.context;
return ( return (
<div style={{cursor: 'pointer', color: 'red'}} role='button'>Delete account</div> <div style={{cursor: 'pointer', color: 'red'}} role='button'>{t('Delete account')}</div>
); );
} }
@ -89,10 +93,12 @@ export default class AccountProfilePage extends React.Component {
} }
renderHeader() { renderHeader() {
const {t} = this.context;
return ( return (
<header className='gh-portal-detail-header'> <header className='gh-portal-detail-header'>
<BackButton brandColor={this.context.brandColor} hidden={!this.context.lastPage} onClick={e => this.onBack(e)} /> <BackButton brandColor={this.context.brandColor} hidden={!this.context.lastPage} onClick={e => this.onBack(e)} />
<h3 className='gh-portal-main-title'>Account settings</h3> <h3 className='gh-portal-main-title'>{t('Account settings')}</h3>
</header> </header>
); );
} }
@ -129,13 +135,15 @@ export default class AccountProfilePage extends React.Component {
} }
getInputFields({state, fieldNames}) { getInputFields({state, fieldNames}) {
const {t} = this.context;
const errors = state.errors || {}; const errors = state.errors || {};
const fields = [ const fields = [
{ {
type: 'text', type: 'text',
value: state.name, value: state.name,
placeholder: 'Jamie Larson', placeholder: 'Jamie Larson',
label: 'Name', label: t('Name'),
name: 'name', name: 'name',
required: true, required: true,
errorMessage: errors.name || '' errorMessage: errors.name || ''
@ -144,7 +152,7 @@ export default class AccountProfilePage extends React.Component {
type: 'email', type: 'email',
value: state.email, value: state.email,
placeholder: 'jamie@example.com', placeholder: 'jamie@example.com',
label: 'Email', label: t('Email'),
name: 'email', name: 'email',
required: true, required: true,
errorMessage: errors.email || '' errorMessage: errors.email || ''

View File

@ -7,7 +7,7 @@ import ActionButton from 'components/common/ActionButton';
import {ReactComponent as EmailDeliveryFailedIcon} from 'images/icons/email-delivery-failed.svg'; import {ReactComponent as EmailDeliveryFailedIcon} from 'images/icons/email-delivery-failed.svg';
export default function EmailSuppressedPage() { export default function EmailSuppressedPage() {
const {brandColor, lastPage, onAction, action, site} = useContext(AppContext); const {brandColor, lastPage, onAction, action, site, t} = useContext(AppContext);
useEffect(() => { useEffect(() => {
if (['removeEmailFromSuppressionList:success'].includes(action)) { if (['removeEmailFromSuppressionList:success'].includes(action)) {
@ -26,13 +26,13 @@ export default function EmailSuppressedPage() {
lastPage: 'accountHome' lastPage: 'accountHome'
}); });
onAction('showPopupNotification', { onAction('showPopupNotification', {
message: 'You have been successfully resubscribed' message: t('You have been successfully resubscribed')
}); });
} else { } else {
onAction('back'); onAction('back');
} }
} }
}, [action, onAction, site]); }, [action, onAction, site, t]);
const isRunning = ['removeEmailFromSuppressionList:running', 'refreshMemberData:running'].includes(action); const isRunning = ['removeEmailFromSuppressionList:running', 'refreshMemberData:running'].includes(action);
@ -52,9 +52,9 @@ export default function EmailSuppressedPage() {
<EmailDeliveryFailedIcon className="gh-email-suppressed-page-icon" /> <EmailDeliveryFailedIcon className="gh-email-suppressed-page-icon" />
<div className="gh-email-suppressed-page-text"> <div className="gh-email-suppressed-page-text">
<h3 className="gh-portal-main-title gh-email-suppressed-page-title">Emails disabled</h3> <h3 className="gh-portal-main-title gh-email-suppressed-page-title">{t('Emails disabled')}</h3>
<p> <p>
You're not receiving emails because you either marked a recent message as spam, or because messages could not be delivered to your provided email address. {t('You\'re not receiving emails because you either marked a recent message as spam, or because messages could not be delivered to your provided email address.')}
</p> </p>
</div> </div>
@ -64,7 +64,7 @@ export default function EmailSuppressedPage() {
onClick={handleSubmit} onClick={handleSubmit}
disabled={isRunning} disabled={isRunning}
brandColor={brandColor} brandColor={brandColor}
label="Re-enable emails" label={t('Re-enable emails')}
isRunning={isRunning} isRunning={isRunning}
/> />
</div> </div>

View File

@ -196,7 +196,7 @@ function ErrorPage({error}) {
} }
const ConfirmDialog = ({onConfirm, loading, initialScore}) => { const ConfirmDialog = ({onConfirm, loading, initialScore}) => {
const {onAction, brandColor} = useContext(AppContext); const {onAction, brandColor, t} = useContext(AppContext);
const [score, setScore] = useState(initialScore); const [score, setScore] = useState(initialScore);
const stopPropagation = (event) => { const stopPropagation = (event) => {
@ -223,7 +223,7 @@ const ConfirmDialog = ({onConfirm, loading, initialScore}) => {
return ( return (
<div className="gh-portal-confirm-dialog" onMouseDown={stopPropagation}> <div className="gh-portal-confirm-dialog" onMouseDown={stopPropagation}>
<h1 className="gh-portal-confirm-title">Give feedback on this post</h1> <h1 className="gh-portal-confirm-title">{t('Give feedback on this post')}</h1>
<div className="gh-feedback-buttons-group"> <div className="gh-feedback-buttons-group">
<button <button
@ -232,7 +232,7 @@ const ConfirmDialog = ({onConfirm, loading, initialScore}) => {
onClick={() => setScore(1)} onClick={() => setScore(1)}
> >
<ThumbUpIcon /> <ThumbUpIcon />
More like this {t('More like this')}
</button> </button>
<button <button
@ -241,7 +241,7 @@ const ConfirmDialog = ({onConfirm, loading, initialScore}) => {
onClick={() => setScore(0)} onClick={() => setScore(0)}
> >
<ThumbDownIcon /> <ThumbDownIcon />
Less like this {t('Less like this')}
</button> </button>
</div> </div>
@ -251,7 +251,7 @@ const ConfirmDialog = ({onConfirm, loading, initialScore}) => {
onClick={submit} onClick={submit}
disabled={false} disabled={false}
brandColor={brandColor} brandColor={brandColor}
label="Submit feedback" label={t('Submit feedback')}
isRunning={loading} isRunning={loading}
tabindex="3" tabindex="3"
/> />
@ -274,7 +274,7 @@ const LoadingFeedbackView = ({action, score}) => {
}; };
const ConfirmFeedback = ({positive}) => { const ConfirmFeedback = ({positive}) => {
const {onAction, brandColor} = useContext(AppContext); const {onAction, brandColor, t} = useContext(AppContext);
const icon = positive ? <ThumbUpIcon /> : <ThumbDownIcon />; const icon = positive ? <ThumbUpIcon /> : <ThumbDownIcon />;
@ -285,15 +285,15 @@ const ConfirmFeedback = ({positive}) => {
<div className="gh-feedback-icon"> <div className="gh-feedback-icon">
{icon} {icon}
</div> </div>
<h1 className="gh-portal-main-title">Thanks for the feedback!</h1> <h1 className="gh-portal-main-title">{t('Thanks for the feedback!')}</h1>
<p className="gh-portal-text-center">Your input helps shape what gets published.</p> <p className="gh-portal-text-center">{t('Your input helps shape what gets published.')}</p>
<ActionButton <ActionButton
style={{width: '100%'}} style={{width: '100%'}}
retry={false} retry={false}
onClick = {() => onAction('closePopup')} onClick = {() => onAction('closePopup')}
disabled={false} disabled={false}
brandColor={brandColor} brandColor={brandColor}
label={'Close'} label={t('Close')}
isRunning={false} isRunning={false}
tabindex='3' tabindex='3'
classes={'sticky bottom'} classes={'sticky bottom'}

View File

@ -27,12 +27,14 @@ export default class MagicLinkPage extends React.Component {
static contextType = AppContext; static contextType = AppContext;
renderFormHeader() { renderFormHeader() {
let popupTitle = `Now check your email!`; const {t} = this.context;
let popupDescription = `A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.`;
let popupTitle = t(`Now check your email!`);
let popupDescription = t(`A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.`);
if (this.context.lastPage === 'signup') { if (this.context.lastPage === 'signup') {
popupTitle = `Now check your email!`; popupTitle = t(`Now check your email!`);
popupDescription = `To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!`; popupDescription = t(`To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!`);
} }
return ( return (
@ -47,13 +49,15 @@ export default class MagicLinkPage extends React.Component {
} }
renderLoginMessage() { renderLoginMessage() {
const {t} = this.context;
return ( return (
<> <>
<div <div
style={{color: '#1d1d1d', fontWeight: 'bold', cursor: 'pointer'}} style={{color: '#1d1d1d', fontWeight: 'bold', cursor: 'pointer'}}
onClick={() => this.context.onAction('switchPage', {page: 'signin'})} onClick={() => this.context.onAction('switchPage', {page: 'signin'})}
> >
Back to Log in {t('Back to Log in')}
</div> </div>
</> </>
); );
@ -64,7 +68,9 @@ export default class MagicLinkPage extends React.Component {
} }
renderCloseButton() { renderCloseButton() {
const label = 'Close'; const {t} = this.context;
const label = t('Close');
return ( return (
<ActionButton <ActionButton
style={{width: '100%'}} style={{width: '100%'}}

View File

@ -65,7 +65,7 @@ function NewsletterPrefs({subscribedNewsletters, setSubscribedNewsletters}) {
} }
export default function NewsletterSelectionPage({pageData, onBack}) { export default function NewsletterSelectionPage({pageData, onBack}) {
const {brandColor, site, onAction, action} = useContext(AppContext); const {brandColor, site, onAction, action, t} = useContext(AppContext);
const siteNewsletters = getSiteNewsletters({site}); const siteNewsletters = getSiteNewsletters({site});
const defaultNewsletters = siteNewsletters.filter((d) => { const defaultNewsletters = siteNewsletters.filter((d) => {
return d.subscribe_on_signup; return d.subscribe_on_signup;
@ -76,10 +76,10 @@ export default function NewsletterSelectionPage({pageData, onBack}) {
if (action === 'signup:running') { if (action === 'signup:running') {
isRunning = true; isRunning = true;
} }
let label = 'Continue'; let label = t('Continue');
let retry = false; let retry = false;
if (action === 'signup:failed') { if (action === 'signup:failed') {
label = 'Retry'; label = t('Retry');
retry = true; retry = true;
} }
@ -88,7 +88,7 @@ export default function NewsletterSelectionPage({pageData, onBack}) {
const [subscribedNewsletters, setSubscribedNewsletters] = useState(defaultNewsletters); const [subscribedNewsletters, setSubscribedNewsletters] = useState(defaultNewsletters);
return ( return (
<div className='gh-portal-content with-footer gh-portal-newsletter-selection'> <div className='gh-portal-content with-footer gh-portal-newsletter-selection'>
<p className="gh-portal-text-center gh-portal-text-large">Choose your newsletters</p> <p className="gh-portal-text-center gh-portal-text-large">{t('Choose your newsletters')}</p>
<div className='gh-portal-section'> <div className='gh-portal-section'>
<div className='gh-portal-list'> <div className='gh-portal-list'>
<NewsletterPrefs <NewsletterPrefs
@ -124,7 +124,7 @@ export default function NewsletterSelectionPage({pageData, onBack}) {
onClick = {() => { onClick = {() => {
onBack(); onBack();
}}> }}>
<span>Choose a different plan</span> <span>{t('Choose a different plan')}</span>
</button> </button>
</div> </div>
</div> </div>

View File

@ -152,14 +152,14 @@ export default class OfferPage extends React.Component {
getInputFields({state, fieldNames}) { getInputFields({state, fieldNames}) {
const {portal_name: portalName} = this.context.site; const {portal_name: portalName} = this.context.site;
const {member} = this.context; const {member, t} = this.context;
const errors = state.errors || {}; const errors = state.errors || {};
const fields = [ const fields = [
{ {
type: 'email', type: 'email',
value: member?.email || state.email, value: member?.email || state.email,
placeholder: 'jamie@example.com', placeholder: 'jamie@example.com',
label: 'Email', label: t('Email'),
name: 'email', name: 'email',
disabled: !!member, disabled: !!member,
required: true, required: true,
@ -181,7 +181,7 @@ export default class OfferPage extends React.Component {
type: 'text', type: 'text',
value: member?.name || state.name, value: member?.name || state.name,
placeholder: 'Jamie Larson', placeholder: 'Jamie Larson',
label: 'Name', label: t('Name'),
name: 'name', name: 'name',
disabled: !!member, disabled: !!member,
required: true, required: true,
@ -307,22 +307,22 @@ export default class OfferPage extends React.Component {
} }
renderSubmitButton() { renderSubmitButton() {
const {action, brandColor} = this.context; const {action, brandColor, t} = this.context;
const {pageData: offer} = this.context; const {pageData: offer} = this.context;
let label = 'Continue'; let label = t('Continue');
if (offer.type === 'trial') { if (offer.type === 'trial') {
label = 'Start ' + offer.amount + '-day free trial'; label = t('Start {{amount}}-day free trial', {amount: offer.amount});
} }
let isRunning = false; let isRunning = false;
if (action === 'signup:running') { if (action === 'signup:running') {
label = 'Sending...'; label = t('Sending...');
isRunning = true; isRunning = true;
} }
let retry = false; let retry = false;
if (action === 'signup:failed') { if (action === 'signup:failed') {
label = 'Retry'; label = t('Retry');
retry = true; retry = true;
} }
@ -347,16 +347,16 @@ export default class OfferPage extends React.Component {
if (member) { if (member) {
return null; return null;
} }
const {brandColor, onAction} = this.context; const {brandColor, onAction, t} = this.context;
return ( return (
<div className='gh-portal-signup-message'> <div className='gh-portal-signup-message'>
<div>Already a member?</div> <div>{t('Already a member?')}</div>
<button <button
className='gh-portal-btn gh-portal-btn-link' className='gh-portal-btn gh-portal-btn-link'
style={{color: brandColor}} style={{color: brandColor}}
onClick={() => onAction('switchPage', {page: 'signin'})} onClick={() => onAction('switchPage', {page: 'signin'})}
> >
<span>Sign in</span> <span>{t('Sign in')}</span>
</button> </button>
</div> </div>
); );
@ -477,15 +477,15 @@ export default class OfferPage extends React.Component {
} }
renderProductLabel({product, offer}) { renderProductLabel({product, offer}) {
const {site} = this.context; const {site, t} = this.context;
if (hasMultipleProductsFeature({site})) { if (hasMultipleProductsFeature({site})) {
return ( return (
<h4 className="gh-portal-plan-name">{product.name} - {(offer.cadence === 'month' ? 'Monthly' : 'Yearly')}</h4> <h4 className="gh-portal-plan-name">{product.name} - {(offer.cadence === 'month' ? t('Monthly') : t('Yearly'))}</h4>
); );
} }
return ( return (
<h4 className="gh-portal-plan-name">{(offer.cadence === 'month' ? 'Monthly' : 'Yearly')}</h4> <h4 className="gh-portal-plan-name">{(offer.cadence === 'month' ? t('Monthly') : t('Yearly'))}</h4>
); );
} }
@ -520,6 +520,8 @@ export default class OfferPage extends React.Component {
} }
renderProductCard({product, offer, currencyClass, updatedPrice, price, benefits}) { renderProductCard({product, offer, currencyClass, updatedPrice, price, benefits}) {
const {t} = this.context;
if (this.state.showNewsletterSelection) { if (this.state.showNewsletterSelection) {
return null; return null;
} }
@ -527,7 +529,7 @@ export default class OfferPage extends React.Component {
<> <>
<div className='gh-portal-product-card top'> <div className='gh-portal-product-card top'>
<div className='gh-portal-product-card-header'> <div className='gh-portal-product-card-header'>
<h4 className="gh-portal-product-name">{product.name} - {(offer.cadence === 'month' ? 'Monthly' : 'Yearly')}</h4> <h4 className="gh-portal-product-name">{product.name} - {(offer.cadence === 'month' ? t('Monthly') : t('Yearly'))}</h4>
{this.renderOldTierPrice({offer, price})} {this.renderOldTierPrice({offer, price})}
{this.renderUpdatedTierPrice({offer, currencyClass, updatedPrice, price})} {this.renderUpdatedTierPrice({offer, currencyClass, updatedPrice, price})}
{this.renderOfferMessage({offer, product, price})} {this.renderOfferMessage({offer, product, price})}

View File

@ -56,13 +56,15 @@ export default class SigninPage extends React.Component {
} }
getInputFields({state}) { getInputFields({state}) {
const {t} = this.context;
const errors = state.errors || {}; const errors = state.errors || {};
const fields = [ const fields = [
{ {
type: 'email', type: 'email',
value: state.email, value: state.email,
placeholder: 'jamie@example.com', placeholder: 'jamie@example.com',
label: 'Email', label: t('Email'),
name: 'email', name: 'email',
required: true, required: true,
errorMessage: errors.email || '', errorMessage: errors.email || '',
@ -73,13 +75,13 @@ export default class SigninPage extends React.Component {
} }
renderSubmitButton() { renderSubmitButton() {
const {action} = this.context; const {action, t} = this.context;
let retry = false; let retry = false;
const isRunning = (action === 'signin:running'); const isRunning = (action === 'signin:running');
let label = isRunning ? 'Sending login link...' : 'Continue'; let label = isRunning ? t('Sending login link...') : t('Continue');
const disabled = isRunning ? true : false; const disabled = isRunning ? true : false;
if (action === 'signin:failed') { if (action === 'signin:failed') {
label = 'Retry'; label = t('Retry');
retry = true; retry = true;
} }
return ( return (
@ -97,17 +99,17 @@ export default class SigninPage extends React.Component {
} }
renderSignupMessage() { renderSignupMessage() {
const brandColor = this.context.brandColor; const {brandColor, t} = this.context;
return ( return (
<div className='gh-portal-signup-message'> <div className='gh-portal-signup-message'>
<div>Don't have an account?</div> <div>{t('Don\'t have an account?')}</div>
<button <button
data-test-button='signup-switch' data-test-button='signup-switch'
className='gh-portal-btn gh-portal-btn-link' className='gh-portal-btn gh-portal-btn-link'
style={{color: brandColor}} style={{color: brandColor}}
onClick={() => this.context.onAction('switchPage', {page: 'signup'})} onClick={() => this.context.onAction('switchPage', {page: 'signup'})}
> >
<span>Sign up</span> <span>{t('Sign up')}</span>
</button> </button>
</div> </div>
); );
@ -143,11 +145,12 @@ export default class SigninPage extends React.Component {
renderFormHeader() { renderFormHeader() {
// const siteTitle = this.context.site.title || 'Site Title'; // const siteTitle = this.context.site.title || 'Site Title';
const {t} = this.context;
return ( return (
<header className='gh-portal-signin-header'> <header className='gh-portal-signin-header'>
{this.renderSiteLogo()} {this.renderSiteLogo()}
<h1 className="gh-portal-main-title">Sign in</h1> <h1 className="gh-portal-main-title">{t('Sign in')}</h1>
</header> </header>
); );
} }

View File

@ -361,7 +361,7 @@ class SignupPage extends React.Component {
} }
getInputFields({state, fieldNames}) { getInputFields({state, fieldNames}) {
const {portal_name: portalName} = this.context.site; const {site: {portal_name: portalName}, t} = this.context;
const errors = state.errors || {}; const errors = state.errors || {};
const fields = [ const fields = [
@ -369,7 +369,7 @@ class SignupPage extends React.Component {
type: 'email', type: 'email',
value: state.email, value: state.email,
placeholder: 'jamie@example.com', placeholder: 'jamie@example.com',
label: 'Email', label: t('Email'),
name: 'email', name: 'email',
required: true, required: true,
tabindex: 2, tabindex: 2,
@ -383,7 +383,7 @@ class SignupPage extends React.Component {
type: 'text', type: 'text',
value: state.name, value: state.name,
placeholder: 'Jamie Larson', placeholder: 'Jamie Larson',
label: 'Name', label: t('Name'),
name: 'name', name: 'name',
required: true, required: true,
tabindex: 1, tabindex: 1,
@ -400,29 +400,29 @@ class SignupPage extends React.Component {
} }
renderSubmitButton() { renderSubmitButton() {
const {action, site, brandColor, pageQuery} = this.context; const {action, site, brandColor, pageQuery, t} = this.context;
if (isInviteOnlySite({site, pageQuery})) { if (isInviteOnlySite({site, pageQuery})) {
return null; return null;
} }
let label = 'Continue'; let label = t('Continue');
const showOnlyFree = pageQuery === 'free' && hasFreeProductPrice({site}); const showOnlyFree = pageQuery === 'free' && hasFreeProductPrice({site});
if (hasOnlyFreePlan({site}) || showOnlyFree) { if (hasOnlyFreePlan({site}) || showOnlyFree) {
label = 'Sign up'; label = t('Sign up');
} else { } else {
return null; return null;
} }
let isRunning = false; let isRunning = false;
if (action === 'signup:running') { if (action === 'signup:running') {
label = 'Sending...'; label = t('Sending...');
isRunning = true; isRunning = true;
} }
let retry = false; let retry = false;
if (action === 'signup:failed') { if (action === 'signup:failed') {
label = 'Retry'; label = t('Retry');
retry = true; retry = true;
} }
@ -456,11 +456,11 @@ class SignupPage extends React.Component {
} }
renderFreeTrialMessage() { renderFreeTrialMessage() {
const {site} = this.context; const {site, t} = this.context;
if (hasFreeTrialTier({site}) && !isInviteOnlySite({site})) { if (hasFreeTrialTier({site}) && !isInviteOnlySite({site})) {
return ( return (
<p className='gh-portal-free-trial-notification' data-testid="free-trial-notification-text"> <p className='gh-portal-free-trial-notification' data-testid="free-trial-notification-text">
After a free trial ends, you will be charged the regular price for the tier youve chosen. You can always cancel before then. {t('After a free trial ends, you will be charged the regular price for the tier you\'ve chosen. You can always cancel before then.')}
</p> </p>
); );
} }
@ -468,19 +468,19 @@ class SignupPage extends React.Component {
} }
renderLoginMessage() { renderLoginMessage() {
const {brandColor, onAction} = this.context; const {brandColor, onAction, t} = this.context;
return ( return (
<div> <div>
{this.renderFreeTrialMessage()} {this.renderFreeTrialMessage()}
<div className='gh-portal-signup-message'> <div className='gh-portal-signup-message'>
<div>Already a member?</div> <div>{t('Already a member?')}</div>
<button <button
data-test-button='signin-switch' data-test-button='signin-switch'
className='gh-portal-btn gh-portal-btn-link' className='gh-portal-btn gh-portal-btn-link'
style={{color: brandColor}} style={{color: brandColor}}
onClick={() => onAction('switchPage', {page: 'signin'})} onClick={() => onAction('switchPage', {page: 'signin'})}
> >
<span>Sign in</span> <span>{t('Sign in')}</span>
</button> </button>
</div> </div>
</div> </div>
@ -489,7 +489,7 @@ class SignupPage extends React.Component {
renderForm() { renderForm() {
const fields = this.getInputFields({state: this.state}); const fields = this.getInputFields({state: this.state});
const {site, pageQuery} = this.context; const {site, pageQuery, t} = this.context;
if (this.state.showNewsletterSelection) { if (this.state.showNewsletterSelection) {
return ( return (
@ -512,7 +512,7 @@ class SignupPage extends React.Component {
className='gh-portal-invite-only-notification' className='gh-portal-invite-only-notification'
data-testid="invite-only-notification-text" data-testid="invite-only-notification-text"
> >
This site is invite-only, contact the owner for access. {t('This site is invite-only, contact the owner for access.')}
</p> </p>
{this.renderLoginMessage()} {this.renderLoginMessage()}
</div> </div>

View File

@ -41,7 +41,7 @@ async function updateMemberNewsletters({api, memberUuid, newsletters, enableComm
} }
export default function UnsubscribePage() { export default function UnsubscribePage() {
const {site, pageData, onAction} = useContext(AppContext); const {site, pageData, onAction, t} = useContext(AppContext);
const api = setupGhostApi({siteUrl: site.url}); const api = setupGhostApi({siteUrl: site.url});
const [member, setMember] = useState(); const [member, setMember] = useState();
const siteNewsletters = getSiteNewsletters({site}); const siteNewsletters = getSiteNewsletters({site});
@ -101,9 +101,9 @@ export default function UnsubscribePage() {
<div class="gh-feedback-icon gh-feedback-icon-error"> <div class="gh-feedback-icon gh-feedback-icon-error">
<WarningIcon /> <WarningIcon />
</div> </div>
<h1 className="gh-portal-main-title">That didn't go to plan</h1> <h1 className="gh-portal-main-title">{t('That didn\'t go to plan')}</h1>
<div> <div>
<p className="gh-portal-text-center">We couldn't unsubscribe you as the email address was not found. Please contact the site owner.</p> <p className="gh-portal-text-center">{t('We couldn\'t unsubscribe you as the email address was not found. Please contact the site owner.')}</p>
</div> </div>
<ActionButton <ActionButton
style={{width: '100%'}} style={{width: '100%'}}
@ -111,7 +111,7 @@ export default function UnsubscribePage() {
onClick = {() => onAction('closePopup')} onClick = {() => onAction('closePopup')}
disabled={false} disabled={false}
brandColor='#000000' brandColor='#000000'
label={'Close'} label={t('Close')}
isRunning={false} isRunning={false}
tabindex='3' tabindex='3'
classes={'sticky bottom'} classes={'sticky bottom'}
@ -126,7 +126,7 @@ export default function UnsubscribePage() {
<div className='gh-portal-content gh-portal-unsubscribe with-footer'> <div className='gh-portal-content gh-portal-unsubscribe with-footer'>
<CloseButton /> <CloseButton />
<AccountHeader /> <AccountHeader />
<h1 className="gh-portal-main-title">Successfully unsubscribed</h1> <h1 className="gh-portal-main-title">{t('Successfully unsubscribed')}</h1>
<div> <div>
<p className='gh-portal-text-center'><strong>{member?.email}</strong> will no longer receive this newsletter.</p> <p className='gh-portal-text-center'><strong>{member?.email}</strong> will no longer receive this newsletter.</p>
<p className='gh-portal-text-center'>Didn't mean to do this? Manage your preferences <p className='gh-portal-text-center'>Didn't mean to do this? Manage your preferences
@ -182,7 +182,7 @@ export default function UnsubscribePage() {
setSubscribedNewsletters([]); setSubscribedNewsletters([]);
onAction('showPopupNotification', { onAction('showPopupNotification', {
action: 'updated:success', action: 'updated:success',
message: `Email preference updated.` message: t(`Email preference updated.`)
}); });
const updatedMember = await api.member.updateNewsletters({uuid: pageData.uuid, newsletters: [], enableCommentNotifications: false}); const updatedMember = await api.member.updateNewsletters({uuid: pageData.uuid, newsletters: [], enableCommentNotifications: false});
setMember(updatedMember); setMember(updatedMember);