0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-10 23:36:14 -05:00

Wrapped public facing strings with translation function

refs: https://github.com/TryGhost/Ghost/issues/16628
This commit is contained in:
Sam Lord 2023-05-25 13:59:22 +01:00 committed by Sam Lord
parent 5816217019
commit 8135ef74f7
11 changed files with 122 additions and 60 deletions

View file

@ -33,10 +33,12 @@
"eslintConfig": {
"extends": [
"react-app",
"plugin:ghost/browser"
"plugin:ghost/browser",
"plugin:i18next/recommended"
],
"plugins": [
"ghost"
"ghost",
"i18next"
]
},
"browserslist": {
@ -60,7 +62,7 @@
},
"devDependencies": {
"@babel/eslint-parser": "7.21.8",
"@doist/react-interpolate": "0.4.0",
"@doist/react-interpolate": "0.4.1",
"@sentry/react": "7.52.1",
"@sentry/tracing": "7.53.0",
"@testing-library/jest-dom": "5.16.5",
@ -73,6 +75,7 @@
"cross-fetch": "3.1.6",
"eslint": "8.37.0",
"eslint-config-react-app": "7.0.1",
"eslint-plugin-i18next": "^6.0.1",
"jsdom": "22.0.0",
"react": "17.0.2",
"react-dom": "17.0.2",

View file

@ -219,7 +219,13 @@ export const GlobalStyles = `
padding: 10vmin 28px;
}
.gh-mobile-shortener {
.gh-desktop-only {
display: none;
}
}
@media (min-width: 481px) {
.gh-mobile-only {
display: none;
}
}

View file

@ -26,6 +26,7 @@ const Styles = () => {
};
const NotificationText = ({type, status, context}) => {
const t = context.t;
const signinPortalLink = getPortalLink({page: 'signin', siteUrl: context.site.url});
const singupPortalLink = getPortalLink({page: 'signup', siteUrl: context.site.url});
@ -33,16 +34,18 @@ const NotificationText = ({type, status, context}) => {
const firstname = context.member.firstname || '';
return (
<p>
Welcome back{(firstname ? ', ' + firstname : '')}!<br />You've successfully signed in.
{firstname ? t('Welcome back, {{name}}!', firstname) : t('Welcome back!')}<br />{t('You\'ve successfully signed in.')}
</p>
);
} else if (type === 'signin' && status === 'error') {
return (
<p>
Could not sign in. Login link expired. <a href={signinPortalLink} target="_parent">Click here to retry</a>
{t('Could not sign in. Login link expired.')} <a href={signinPortalLink} target="_parent">{t('Click here to retry')}</a>
</p>
);
} else if (type === 'signup' && status === 'success') {
// TODO: Wrap these strings with translation function
/* eslint-disable i18next/no-literal-string */
return (
<p>
You've successfully subscribed to <br /><strong>{context.site.title}</strong>
@ -54,41 +57,42 @@ const NotificationText = ({type, status, context}) => {
You've successfully subscribed to <br /><strong>{context.site.title}</strong>
</p>
);
/* eslint-enable i18next/no-literal-string */
} else if (type === 'updateEmail' && status === 'success') {
return (
<p>
Success! Your email is updated.
{t('Success! Your email is updated.')}
</p>
);
} else if (type === 'updateEmail' && status === 'error') {
return (
<p>
Could not update email! Invalid link.
{t('Could not update email! Invalid link.')}
</p>
);
} else if (type === 'signup' && status === 'error') {
return (
<p>
Signup error: Invalid link <br /><a href={singupPortalLink} target="_parent">Click here to retry</a>
{t('Signup error: Invalid link')}<br /><a href={singupPortalLink} target="_parent">{t('Click here to retry')}</a>
</p>
);
} else if (type === 'signup-paid' && status === 'error') {
return (
<p>
Signup error: Invalid link <br /><a href={singupPortalLink} target="_parent">Click here to retry</a>
{t('Signup error: Invalid link')}<br /><a href={singupPortalLink} target="_parent">{t('Click here to retry')}</a>
</p>
);
} else if (type === 'stripe:checkout' && status === 'success') {
if (context.member) {
return (
<p>
Success! Your account is fully activated, you now have access to all content.
{t('Success! Your account is fully activated, you now have access to all content.')}
</p>
);
}
return (
<p>
Success! Check your email for magic link to sign-in.
{t('Success! Check your email for magic link to sign-in.')}
</p>
);
} else if (type === 'stripe:checkout' && status === 'warning') {
@ -96,19 +100,19 @@ const NotificationText = ({type, status, context}) => {
if (context.member) {
return (
<p>
Plan upgrade was cancelled.
{t('Plan upgrade was cancelled.')}
</p>
);
}
return (
<p>
Plan checkout was cancelled.
{t('Plan checkout was cancelled.')}
</p>
);
}
return (
<p>
{status === 'success' ? 'Success' : 'Error'}
{status === 'success' ? t('Success') : t('Error')}
</p>
);
};

View file

@ -207,6 +207,7 @@ export default function NewsletterManagement({
className="gh-portal-btn-text gh-email-faq-page-button"
onClick={() => onAction('switchPage', {page: 'emailReceivingFAQ'})}
>
{/* eslint-disable-next-line i18next/no-literal-string */}
{t('Get help')} &rarr;
</button>
</div>

View file

@ -5,6 +5,8 @@ import {ReactComponent as CheckmarkIcon} from '../../images/icons/checkmark-fill
import {ReactComponent as WarningIcon} from '../../images/icons/warning-fill.svg';
import {getSupportAddress} from '../../utils/helpers';
import {clearURLParams} from '../../utils/notifications';
import Interpolate from '@doist/react-interpolate';
import {SYNTAX_I18NEXT} from '@doist/react-interpolate';
export const PopupNotificationStyles = `
.gh-portal-popupnotification {
@ -110,7 +112,7 @@ const CloseButton = ({hide = false, onClose}) => {
);
};
const NotificationText = ({message, site}) => {
const NotificationText = ({message, site, t}) => {
const supportAddress = getSupportAddress({site});
const supportAddressMail = `mailto:${supportAddress}`;
if (message) {
@ -119,9 +121,18 @@ const NotificationText = ({message, site}) => {
);
}
return (
<p> An unexpected error occured. Please try again or <a href={supportAddressMail} onClick={() => {
<p>
<Interpolate
syntax={SYNTAX_I18NEXT}
string={t('An unexpected error occured. Please try again or <a>contact support</a> if the error persists.')}
mapping={{
// eslint-disable-next-line jsx-a11y/anchor-has-content
a: <a href={supportAddressMail} onClick={() => {
supportAddressMail && window.open(supportAddressMail);
}}>contact support</a> if the error persists.</p>
}}/>
}}
/>
</p>
);
};
@ -187,7 +198,7 @@ export default class PopupNotification extends React.Component {
}
render() {
const {popupNotification, site} = this.context;
const {popupNotification, site, t} = this.context;
const {className} = this.state;
const {type, status, closeable, message} = popupNotification;
const statusClass = status ? ` ${status}` : '';
@ -196,7 +207,7 @@ export default class PopupNotification extends React.Component {
return (
<div className={`gh-portal-popupnotification${statusClass}${slideClass}`} onAnimationEnd={e => this.onAnimationEnd(e)}>
{(status === 'error' ? <WarningIcon className='gh-portal-popupnotification-icon error' alt=''/> : <CheckmarkIcon className='gh-portal-popupnotification-icon success' alt=''/>)}
<NotificationText type={type} status={status} message={message} site={site} />
<NotificationText type={type} status={status} message={message} site={site} t={t} />
<CloseButton hide={!closeable} onClose={e => this.closeNotification(e)}/>
</div>
);

View file

@ -8,7 +8,7 @@ export default class PoweredBy extends React.Component {
render() {
// Note: please do not wrap "Powered by Ghost" in the translation function, as we don't
// want it to be translated
/* eslint-disable i18next/no-literal-string */
return (
<a href='https://ghost.org' target='_blank' rel='noopener noreferrer' onClick={() => {
window.open('https://ghost.org', '_blank');
@ -17,5 +17,6 @@ export default class PoweredBy extends React.Component {
Powered by Ghost
</a>
);
/* eslint-enable i18next/no-literal-string */
}
}

View file

@ -814,7 +814,7 @@ function ProductCards({products, selectedInterval, handleChooseSignup, errors})
}
function YearlyDiscount({discount, trialDays}) {
const {site} = useContext(AppContext);
const {site, t} = useContext(AppContext);
const {portal_plans: portalPlans} = site;
if (discount === 0 || !portalPlans.includes('monthly')) {
@ -824,13 +824,13 @@ function YearlyDiscount({discount, trialDays}) {
if (hasFreeTrialTier({site})) {
return (
<>
<span className="gh-portal-discount-label-trial">{discount}% discount</span>
<span className="gh-portal-discount-label-trial">{t('{{discountPercent}} discount', {discountPercent: discount + '%'})}</span>
</>
);
} else {
return (
<>
<span className="gh-portal-discount-label">{discount}% discount</span>
<span className="gh-portal-discount-label">{t('{{discountPercent}} discount', {discountPercent: discount + '%'})}</span>
</>
);
}
@ -1034,7 +1034,7 @@ function ProductDescription({product, selectedPrice, activePrice}) {
}
function ChangeProductCard({product, onPlanSelect}) {
const {member, site} = useContext(AppContext);
const {member, site, t} = useContext(AppContext);
const {selectedProduct, setSelectedProduct, selectedInterval} = useContext(ProductsContext);
const cardClass = selectedProduct === product.id ? 'gh-portal-product-card checked' : 'gh-portal-product-card';
const monthlyPrice = product.monthlyPrice;
@ -1061,7 +1061,7 @@ function ChangeProductCard({product, onPlanSelect}) {
</div>
{(currentPlan ?
<div className='gh-portal-btn-product'>
<span className='gh-portal-current-plan'><span>Current plan</span></span>
<span className='gh-portal-current-plan'><span>{t('Current plan')}</span></span>
</div>
:
<div className='gh-portal-btn-product'>
@ -1071,7 +1071,7 @@ function ChangeProductCard({product, onPlanSelect}) {
onClick={() => {
onPlanSelect(null, selectedPrice?.id);
}}
>Choose</button>
>{t('Choose')}</button>
</div>)}
</div>
</div>

View file

@ -17,6 +17,7 @@ export default class SiteTitleBackButton extends React.Component {
this.context.onAction('closePopup');
}
}}>
{/* eslint-disable-next-line i18next/no-literal-string */}
<span>&larr; </span> {t('Back')}
</button>
</>

View file

@ -17,7 +17,8 @@ function EmailPreferencesAction() {
? (
<p className="gh-portal-email-notice">
<EmailDeliveryFailedIcon className="gh-portal-email-notice-icon" />
<span>You're <span className="gh-mobile-shortener">currently </span>not receiving emails</span>
<span className="gh-mobile-only">{t('You\'re not receiving emails')}</span>
<span className="gh-desktop-only">{t('You\'re currently not receiving emails')}</span>
</p>
)
: <p>{t('Update your preferences')}</p>

View file

@ -431,7 +431,7 @@ export default class OfferPage extends React.Component {
}
renderOfferTag() {
const {pageData: offer} = this.context;
const {pageData: offer, t} = this.context;
if (offer.amount <= 0) {
return (
@ -441,18 +441,20 @@ export default class OfferPage extends React.Component {
if (offer.type === 'fixed') {
return (
<h5 className="gh-portal-discount-label">{getCurrencySymbol(offer.currency)}{offer.amount / 100} off</h5>
<h5 className="gh-portal-discount-label">{t('{{amount}} off', {
amount: `${getCurrencySymbol(offer.currency)}${offer.amount / 100}`
})}</h5>
);
}
if (offer.type === 'trial') {
return (
<h5 className="gh-portal-discount-label">{offer.amount} days free</h5>
<h5 className="gh-portal-discount-label">{t('{{amount}} days free', {amount: offer.amount})}</h5>
);
}
return (
<h5 className="gh-portal-discount-label">{offer.amount}% off</h5>
<h5 className="gh-portal-discount-label">{t('{{amount}} off', {amount: offer.amount + '%'})}</h5>
);
}
@ -515,32 +517,51 @@ export default class OfferPage extends React.Component {
return '';
}
renderOfferMessage({offer, product, price}) {
const discountDuration = offer.duration;
let durationLabel = '';
renderOfferMessage({offer, product, price, t}) {
const offerMessages = {
forever: t(`{{amount}} off forever.`, {
amount: this.getOffAmount({offer})
}),
firstPeriod: t(`{{amount}} off for first {{period}}.`, {
amount: this.getOffAmount({offer}),
period: offer.cadence
}),
firstNMonths: t(`{{amount}} off for first {{number}} months.`, {
amount: this.getOffAmount({offer}),
number: offer.duration_in_months || ''
})
};
const originalPrice = this.getOriginalPrice({offer, product});
let renewsLabel = '';
const renewsLabel = t(`Renews at {{price}}.`, {price: originalPrice});
let offerLabel = '';
let useRenewsLabel = false;
const discountDuration = offer.duration;
if (discountDuration === 'once') {
durationLabel = `for first ${offer.cadence}`;
renewsLabel = `Renews at ${originalPrice}.`;
offerLabel = offerMessages.firstPeriod;
useRenewsLabel = true;
} else if (discountDuration === 'forever') {
durationLabel = `forever`;
offerLabel = offerMessages.forever;
} else if (discountDuration === 'repeating') {
const durationInMonths = offer.duration_in_months || '';
if (durationInMonths === 1) {
durationLabel = `for first month`;
offerLabel = offerMessages.firstPeriod;
} else {
durationLabel = `for first ${durationInMonths} months`;
offerLabel = offerMessages.firstNMonths;
}
renewsLabel = `Renews at ${originalPrice}.`;
useRenewsLabel = true;
}
if (discountDuration === 'trial') {
return (
<p className="footnote">Try free for {offer.amount} days, then {originalPrice}. <span class="gh-portal-cancel">Cancel anytime.</span></p>
<p className="footnote">{t('Try free for {{amount}} days, then {{originalPrice}}.', {
amount: offer.amount,
originalPrice: originalPrice
})} <span class="gh-portal-cancel">{t('Cancel anytime.')}</span></p>
);
}
return (
<p className="footnote">{this.getOffAmount({offer})} off {durationLabel}. {renewsLabel}</p>
<p className="footnote">{offerLabel} {useRenewsLabel ? renewsLabel : ''}</p>
);
}
@ -625,7 +646,7 @@ export default class OfferPage extends React.Component {
}
render() {
const {pageData: offer, site} = this.context;
const {pageData: offer, site, t} = this.context;
if (!offer) {
return null;
}
@ -647,7 +668,7 @@ export default class OfferPage extends React.Component {
<div className="gh-portal-offer-bar">
<div className="gh-portal-offer-title">
{(offer.display_title ? <h4>{offer.display_title}</h4> : <h4 className='placeholder'>Black Friday</h4>)}
{(offer.display_title ? <h4>{offer.display_title}</h4> : <h4 className='placeholder'>{t('Black Friday')}</h4>)}
{this.renderOfferTag()}
</div>
{(offer.display_description ? <p>{offer.display_description}</p> : '')}

View file

@ -2034,10 +2034,10 @@
resolved "https://registry.yarnpkg.com/@ebay/nice-modal-react/-/nice-modal-react-1.2.10.tgz#6b2406bfce4a5daffc43f5b85f5f238311cdfe93"
integrity sha512-qNp8vQo5kPRwB9bHlkh8lcwH/0KFWpp58X/b9KaLB/gNlJ3W24nCT2l/qBBSnWgV7NEIq25uLowaPS2mbfpZiw==
"@doist/react-interpolate@0.4.0":
version "0.4.0"
resolved "https://registry.yarnpkg.com/@doist/react-interpolate/-/react-interpolate-0.4.0.tgz#b22a408ec78374213e0148473f082dd4c9a72285"
integrity sha512-hGSaTKnY5U6F3/MvuoZfGvtN4nbpL2NhL7c8atQqm0Yn4E2I4ChQQiMxm9/3NEOzd6VfyFxuK9Eo85ogTIO/PA==
"@doist/react-interpolate@0.4.1":
version "0.4.1"
resolved "https://registry.yarnpkg.com/@doist/react-interpolate/-/react-interpolate-0.4.1.tgz#696d14e90ffc849e94a4e3e94578c5eade1590b8"
integrity sha512-JxBJMpDlXByMrA7T6Z+tRdCqXlbnM1ikfasLaqMrWvV4TvvrPuD7dngU/X01iP9tpAycfxasNvbNKFs/QehViQ==
"@elastic/elasticsearch@8.6.0":
version "8.6.0"
@ -16801,6 +16801,14 @@ eslint-plugin-ghost@3.0.0:
eslint-plugin-sort-imports-es6-autofix "0.6.0"
eslint-plugin-unicorn "42.0.0"
eslint-plugin-i18next@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-i18next/-/eslint-plugin-i18next-6.0.1.tgz#b0a289179598b247a73881abccb4b73ea8a5e835"
integrity sha512-aJ0UlZLVqBK7mdXsl3yFQNz72OPUjSmBfcrMjCNxfMp8YVBAlKE83ZfzzTLCMQvFlXZs0TWYTtoteO/ZiZdDdw==
dependencies:
lodash "^4.17.21"
requireindex "~1.1.0"
eslint-plugin-import@^2.25.3:
version "2.27.5"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz#876a6d03f52608a3e5bb439c2550588e51dd6c65"
@ -29227,6 +29235,11 @@ requireindex@^1.2.0:
resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.2.0.tgz#3463cdb22ee151902635aa6c9535d4de9c2ef1ef"
integrity sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==
requireindex@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.1.0.tgz#e5404b81557ef75db6e49c5a72004893fe03e162"
integrity sha512-LBnkqsDE7BZKvqylbmn7lTIVdpx4K/QCduRATpO5R+wtPmky/a8pN1bO2D6wXppn1497AJF9mNjqAXr6bdl9jg==
requires-port@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"