Merge pull request #17559 from overleaf/ds-latam-v2

LATAM Geo Pricing - Introducing split test in LATAM countries Round 2

GitOrigin-RevId: 76ab880262f126f8db7f8e577154d78b5e9abf7a
This commit is contained in:
Davinder Singh 2024-04-08 10:24:10 +01:00 committed by Copybot
parent d7d63e0f51
commit 998d55d159
16 changed files with 291 additions and 25 deletions

View file

@ -359,15 +359,32 @@ async function projectListPage(req, res, next) {
let showInrGeoBanner = false let showInrGeoBanner = false
let showBrlGeoBanner = false let showBrlGeoBanner = false
let showLATAMBanner = false
let recommendedCurrency let recommendedCurrency
if (usersBestSubscription?.type === 'free') { if (usersBestSubscription?.type === 'free') {
const { countryCode } = await GeoIpLookup.promises.getCurrencyCode(req.ip) const latamGeoPricingAssignment =
await SplitTestHandler.promises.getAssignment(
req,
res,
'geo-pricing-latam-v2'
)
const { countryCode, currencyCode } =
await GeoIpLookup.promises.getCurrencyCode(req.ip)
if (countryCode === 'IN') { if (countryCode === 'IN') {
showInrGeoBanner = true showInrGeoBanner = true
} }
showBrlGeoBanner = countryCode === 'BR' showBrlGeoBanner = countryCode === 'BR'
showLATAMBanner =
latamGeoPricingAssignment.variant === 'latam' &&
['MX', 'CO', 'CL', 'PE'].includes(countryCode)
// LATAM Banner needs to know which currency to display
if (showLATAMBanner) {
recommendedCurrency = currencyCode
}
} }
let hasIndividualRecurlySubscription = false let hasIndividualRecurlySubscription = false
@ -426,6 +443,7 @@ async function projectListPage(req, res, next) {
showGroupsAndEnterpriseBanner, showGroupsAndEnterpriseBanner,
groupsAndEnterpriseBannerVariant, groupsAndEnterpriseBannerVariant,
showWritefullPromoBanner, showWritefullPromoBanner,
showLATAMBanner,
recommendedCurrency, recommendedCurrency,
showInrGeoBanner, showInrGeoBanner,
showBrlGeoBanner, showBrlGeoBanner,

View file

@ -37,10 +37,10 @@ async function plansPage(req, res) {
if (GeoIpLookup.isValidCurrencyParam(queryCurrency)) { if (GeoIpLookup.isValidCurrencyParam(queryCurrency)) {
currency = queryCurrency currency = queryCurrency
} }
const { recommendedCurrency, countryCode } = await _getRecommendedCurrency( const { recommendedCurrency, countryCode, geoPricingLATAMTestVariant } =
req, await _getRecommendedCurrency(req, res)
res
) const latamCountryBannerDetails = await getLatamCountryBannerDetails(req, res)
if (recommendedCurrency && currency == null) { if (recommendedCurrency && currency == null) {
currency = recommendedCurrency currency = recommendedCurrency
} }
@ -68,6 +68,7 @@ async function plansPage(req, res) {
const plansPageViewSegmentation = { const plansPageViewSegmentation = {
currency: recommendedCurrency, currency: recommendedCurrency,
countryCode, countryCode,
'geo-pricing-latam-v2': geoPricingLATAMTestVariant,
} }
AnalyticsManager.recordEventForSession( AnalyticsManager.recordEventForSession(
@ -76,6 +77,10 @@ async function plansPage(req, res) {
plansPageViewSegmentation plansPageViewSegmentation
) )
const showLATAMBanner =
geoPricingLATAMTestVariant === 'latam' &&
['MX', 'CO', 'CL', 'PE'].includes(countryCode)
res.render('subscriptions/plans', { res.render('subscriptions/plans', {
title: 'plans_and_pricing', title: 'plans_and_pricing',
currentView, currentView,
@ -93,6 +98,8 @@ async function plansPage(req, res) {
SubscriptionHelper.generateInitialLocalizedGroupPrice(currency), SubscriptionHelper.generateInitialLocalizedGroupPrice(currency),
showInrGeoBanner: countryCode === 'IN', showInrGeoBanner: countryCode === 'IN',
showBrlGeoBanner: countryCode === 'BR', showBrlGeoBanner: countryCode === 'BR',
showLATAMBanner,
latamCountryBannerDetails,
}) })
} }
@ -202,10 +209,10 @@ async function userSubscriptionPage(req, res) {
async function interstitialPaymentPage(req, res) { async function interstitialPaymentPage(req, res) {
const user = SessionManager.getSessionUser(req.session) const user = SessionManager.getSessionUser(req.session)
const { recommendedCurrency, countryCode } = await _getRecommendedCurrency( const { recommendedCurrency, countryCode, geoPricingLATAMTestVariant } =
req, await _getRecommendedCurrency(req, res)
res
) const latamCountryBannerDetails = await getLatamCountryBannerDetails(req, res)
const hasSubscription = const hasSubscription =
await LimitationsManager.promises.userHasV1OrV2Subscription(user) await LimitationsManager.promises.userHasV1OrV2Subscription(user)
@ -217,6 +224,7 @@ async function interstitialPaymentPage(req, res) {
const paywallPlansPageViewSegmentation = { const paywallPlansPageViewSegmentation = {
currency: recommendedCurrency, currency: recommendedCurrency,
countryCode, countryCode,
'geo-pricing-latam-v2': geoPricingLATAMTestVariant,
} }
AnalyticsManager.recordEventForSession( AnalyticsManager.recordEventForSession(
req.session, req.session,
@ -224,6 +232,10 @@ async function interstitialPaymentPage(req, res) {
paywallPlansPageViewSegmentation paywallPlansPageViewSegmentation
) )
const showLATAMBanner =
geoPricingLATAMTestVariant === 'latam' &&
['MX', 'CO', 'CL', 'PE'].includes(countryCode)
res.render('subscriptions/interstitial-payment', { res.render('subscriptions/interstitial-payment', {
title: 'subscribe', title: 'subscribe',
itm_content: req.query?.itm_content, itm_content: req.query?.itm_content,
@ -234,6 +246,8 @@ async function interstitialPaymentPage(req, res) {
showSkipLink, showSkipLink,
showInrGeoBanner: countryCode === 'IN', showInrGeoBanner: countryCode === 'IN',
showBrlGeoBanner: countryCode === 'BR', showBrlGeoBanner: countryCode === 'BR',
showLATAMBanner,
latamCountryBannerDetails,
}) })
} }
} }
@ -551,16 +565,65 @@ async function _getRecommendedCurrency(req, res) {
const countryCode = currencyLookup.countryCode const countryCode = currencyLookup.countryCode
let recommendedCurrency = currencyLookup.currencyCode let recommendedCurrency = currencyLookup.currencyCode
if (['MXN', 'COP', 'CLP', 'PEN'].includes(recommendedCurrency)) { const assignmentLATAM = await SplitTestHandler.promises.getAssignment(
req,
res,
'geo-pricing-latam-v2'
)
if (
['MXN', 'COP', 'CLP', 'PEN'].includes(recommendedCurrency) &&
assignmentLATAM?.variant === 'default'
) {
recommendedCurrency = GeoIpLookup.DEFAULT_CURRENCY_CODE recommendedCurrency = GeoIpLookup.DEFAULT_CURRENCY_CODE
} }
return { return {
recommendedCurrency, recommendedCurrency,
countryCode, countryCode,
geoPricingLATAMTestVariant: assignmentLATAM?.variant,
} }
} }
async function getLatamCountryBannerDetails(req, res) {
const userId = SessionManager.getLoggedInUserId(req.session)
let ip = req.ip
if (
req.query?.ip &&
(await AuthorizationManager.promises.isUserSiteAdmin(userId))
) {
ip = req.query.ip
}
const currencyLookup = await GeoIpLookup.promises.getCurrencyCode(ip)
const countryCode = currencyLookup.countryCode
const latamCountryBannerDetails = {}
switch (countryCode) {
case `MX`:
latamCountryBannerDetails.latamCountryFlag = '🇲🇽'
latamCountryBannerDetails.country = 'Mexico'
latamCountryBannerDetails.discount = '25%'
break
case `CO`:
latamCountryBannerDetails.latamCountryFlag = '🇨🇴'
latamCountryBannerDetails.country = 'Colombia'
latamCountryBannerDetails.discount = '60%'
break
case `CL`:
latamCountryBannerDetails.latamCountryFlag = '🇨🇱'
latamCountryBannerDetails.country = 'Chile'
latamCountryBannerDetails.discount = '30%'
break
case `PE`:
latamCountryBannerDetails.latamCountryFlag = '🇵🇪'
latamCountryBannerDetails.country = 'Peru'
latamCountryBannerDetails.discount = '40%'
break
}
return latamCountryBannerDetails
}
module.exports = { module.exports = {
plansPage: expressify(plansPage), plansPage: expressify(plansPage),
userSubscriptionPage: expressify(userSubscriptionPage), userSubscriptionPage: expressify(userSubscriptionPage),

View file

@ -31,6 +31,7 @@ block append meta
meta(name="ol-showInrGeoBanner" data-type="boolean" content=showInrGeoBanner) meta(name="ol-showInrGeoBanner" data-type="boolean" content=showInrGeoBanner)
meta(name="ol-showBrlGeoBanner" data-type="boolean" content=showBrlGeoBanner) meta(name="ol-showBrlGeoBanner" data-type="boolean" content=showBrlGeoBanner)
meta(name="ol-recommendedCurrency" data-type="string" content=recommendedCurrency) meta(name="ol-recommendedCurrency" data-type="string" content=recommendedCurrency)
meta(name="ol-showLATAMBanner" data-type="boolean" content=showLATAMBanner)
meta(name="ol-groupSubscriptionsPendingEnrollment" data-type="json" content=groupSubscriptionsPendingEnrollment) meta(name="ol-groupSubscriptionsPendingEnrollment" data-type="json" content=groupSubscriptionsPendingEnrollment)
meta(name="ol-hasIndividualRecurlySubscription" data-type="boolean" content=hasIndividualRecurlySubscription) meta(name="ol-hasIndividualRecurlySubscription" data-type="boolean" content=hasIndividualRecurlySubscription)
meta(name="ol-newNotificationStyle" data-type="boolean" content=newNotificationStyle) meta(name="ol-newNotificationStyle" data-type="boolean" content=newNotificationStyle)

View file

@ -23,6 +23,9 @@ block content
if showBrlGeoBanner if showBrlGeoBanner
div.notification.notification-type-success.text-centered div.notification.notification-type-success.text-centered
div.notification-content !{translate("brl_discount_offer_plans_page_banner", {flag: '🇧🇷'})} div.notification-content !{translate("brl_discount_offer_plans_page_banner", {flag: '🇧🇷'})}
if showLATAMBanner
div.notification.notification-type-success.text-centered
div.notification-content !{translate("latam_discount_offer_plans_page_banner", {flag: latamCountryBannerDetails.latamCountryFlag, country: latamCountryBannerDetails.country, discount: latamCountryBannerDetails.discount })}
.row .row
.col-md-12 .col-md-12

View file

@ -21,6 +21,9 @@ block content
if showBrlGeoBanner if showBrlGeoBanner
div.notification.notification-type-success.text-centered div.notification.notification-type-success.text-centered
div.notification-content !{translate("brl_discount_offer_plans_page_banner", {flag: '🇧🇷'})} div.notification-content !{translate("brl_discount_offer_plans_page_banner", {flag: '🇧🇷'})}
if showLATAMBanner
div.notification.notification-type-success.text-centered
div.notification-content !{translate("latam_discount_offer_plans_page_banner", {flag: latamCountryBannerDetails.latamCountryFlag, country: latamCountryBannerDetails.country, discount: latamCountryBannerDetails.discount })}
.row .row
.col-md-12 .col-md-12

View file

@ -107,8 +107,6 @@
"binary_history_error": "", "binary_history_error": "",
"blank_project": "", "blank_project": "",
"blocked_filename": "", "blocked_filename": "",
"brl_discount_modal_info": "",
"brl_discount_modal_title": "",
"browser": "", "browser": "",
"bulk_accept_confirm": "", "bulk_accept_confirm": "",
"bulk_reject_confirm": "", "bulk_reject_confirm": "",
@ -655,6 +653,8 @@
"last_resort_trouble_shooting_guide": "", "last_resort_trouble_shooting_guide": "",
"last_updated_date_by_x": "", "last_updated_date_by_x": "",
"last_used": "", "last_used": "",
"latam_discount_modal_info": "",
"latam_discount_modal_title": "",
"latex_places_figures_according_to_a_special_algorithm": "", "latex_places_figures_according_to_a_special_algorithm": "",
"latex_places_tables_according_to_a_special_algorithm": "", "latex_places_tables_according_to_a_special_algorithm": "",
"layout": "", "layout": "",

View file

@ -85,19 +85,24 @@ export default function BRLBanner() {
return ( return (
<AccessibleModal show={showModal} onHide={handleHide}> <AccessibleModal show={showModal} onHide={handleHide}>
<Modal.Header closeButton> <Modal.Header closeButton>
<Modal.Title>{t('brl_discount_modal_title')}</Modal.Title> <Modal.Title>{t('latam_discount_modal_title')}</Modal.Title>
</Modal.Header> </Modal.Header>
<Modal.Body className="modal-body-share"> <Modal.Body className="modal-body-share">
<p> <p>
<img <img
alt={t('brl_discount_modal_title')} alt={t('latam_discount_modal_title')}
src="/img/subscriptions/blr-discount-modal.png" src="/img/subscriptions/blr-discount-modal.png"
style={{ style={{
width: '100%', width: '100%',
}} }}
/> />
</p> </p>
<p>{t('brl_discount_modal_info')}</p> <p>
{t('latam_discount_modal_info', {
discount: '50%',
currencyName: 'Brazilian Reais',
})}
</p>
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<Button bsStyle="default" onClick={handleMaybeLater}> <Button bsStyle="default" onClick={handleMaybeLater}>

View file

@ -0,0 +1,157 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import usePersistedState from '../../../../../shared/hooks/use-persisted-state'
import * as eventTracking from '../../../../../infrastructure/event-tracking'
import { Modal, Button } from 'react-bootstrap'
import AccessibleModal from '../../../../../shared/components/accessible-modal'
import { useTranslation } from 'react-i18next'
import getMeta from '@/utils/meta'
const LATAM_CURRENCIES = {
MXN: {
name: 'Mexican Pesos',
countryCode: 'MX',
discountCode: '25',
imageSource: '/img/subscriptions/mexico-discount-modal.png',
},
COP: {
name: 'Colombian Pesos',
countryCode: 'CO',
discountCode: '60',
imageSource: '/img/subscriptions/colombia-discount-modal.png',
},
CLP: {
name: 'Chilean Pesos',
countryCode: 'CL',
discountCode: '30',
imageSource: '/img/subscriptions/chile-discount-modal.png',
},
PEN: {
name: 'Peruvian Soles',
countryCode: 'PE',
discountCode: '40',
imageSource: '/img/subscriptions/peru-discount-modal.png',
},
}
export default function LATAMBanner() {
const { t } = useTranslation()
const [dismissedUntil, setDismissedUntil] = usePersistedState<
Date | undefined
>(`has_dismissed_latam_banner_until`)
const viewEventSent = useRef<boolean>(false)
const [showModal, setShowModal] = useState(true)
const currency = getMeta('ol-recommendedCurrency')
const {
imageSource,
name: currencyName,
discountCode,
countryCode,
} = LATAM_CURRENCIES[currency as keyof typeof LATAM_CURRENCIES]
useEffect(() => {
if (dismissedUntil && new Date(dismissedUntil) > new Date()) {
return
}
if (!viewEventSent.current) {
eventTracking.sendMB('promo-prompt', {
location: 'dashboard-modal',
name: 'geo-pricing',
page: '/project',
content: 'modal',
country: countryCode,
})
viewEventSent.current = true
}
}, [dismissedUntil, countryCode])
const handleClick = useCallback(() => {
eventTracking.sendMB('promo-click', {
location: 'dashboard-modal',
name: 'geo-pricing',
page: '/project',
content: 'modal',
country: countryCode,
type: 'click',
})
setShowModal(false)
window.open('/user/subscription/plans')
}, [countryCode])
const bannerDismissed = useCallback(() => {
eventTracking.sendMB('promo-dismiss', {
location: 'dashboard-modal',
name: 'geo-pricing',
page: '/project',
content: 'modal',
country: countryCode,
})
const until = new Date()
until.setDate(until.getDate() + 30) // 30 days
setDismissedUntil(until)
}, [setDismissedUntil, countryCode])
const handleHide = useCallback(() => {
setShowModal(false)
bannerDismissed()
}, [bannerDismissed])
const handleMaybeLater = useCallback(() => {
eventTracking.sendMB('promo-click', {
location: 'dashboard-modal',
name: 'geo-pricing',
page: '/project',
content: 'modal',
country: countryCode,
type: 'pause',
})
setShowModal(false)
const until = new Date()
until.setDate(until.getDate() + 1) // 1 day
setDismissedUntil(until)
}, [setDismissedUntil, countryCode])
if (dismissedUntil && new Date(dismissedUntil) > new Date()) {
return null
}
// Safety, but should always be a valid LATAM currency if ol-showLATAMBanner is true
if (!(currency in LATAM_CURRENCIES)) {
return null
}
return (
<AccessibleModal show={showModal} onHide={handleHide} backdrop="static">
<Modal.Header closeButton>
<Modal.Title>{t('latam_discount_modal_title')}</Modal.Title>
</Modal.Header>
<Modal.Body className="modal-body-share">
<p>
<img
alt={t('latam_discount_modal_title')}
src={imageSource}
style={{
width: '100%',
}}
/>
</p>
<p>
{t('latam_discount_modal_info', {
discount: discountCode,
currencyName,
})}
</p>
</Modal.Body>
<Modal.Footer>
<Button bsStyle="default" onClick={handleMaybeLater}>
{t('maybe_later')}
</Button>
<Button type="button" bsStyle="primary" onClick={handleClick}>
{t('get_discounted_plan')}
</Button>
</Modal.Footer>
</AccessibleModal>
)
}

View file

@ -0,0 +1,19 @@
import getMeta from '@/utils/meta'
import BRLBanner from './ads/brl-banner'
import INRBanner from './ads/inr-banner'
import LATAMBanner from './ads/latam-banner'
function GeoBanners() {
const showInrGeoBanner = getMeta('ol-showInrGeoBanner', false)
const showBrlGeoBanner = getMeta('ol-showBrlGeoBanner', false)
const showLATAMBanner = getMeta('ol-showLATAMBanner', false)
return (
<>
{showBrlGeoBanner && <BRLBanner />}
{showLATAMBanner && <LATAMBanner />}
{showInrGeoBanner && <INRBanner />}
</>
)
}
export default GeoBanners

View file

@ -6,13 +6,12 @@ import ReconfirmationInfo from './groups/affiliation/reconfirmation-info'
import GroupsAndEnterpriseBanner from './groups-and-enterprise-banner' import GroupsAndEnterpriseBanner from './groups-and-enterprise-banner'
import WritefullPremiumPromoBanner from './writefull-premium-promo-banner' import WritefullPremiumPromoBanner from './writefull-premium-promo-banner'
import GroupSsoSetupSuccess from './groups/group-sso-setup-success' import GroupSsoSetupSuccess from './groups/group-sso-setup-success'
import INRBanner from './ads/inr-banner'
import getMeta from '../../../../utils/meta' import getMeta from '../../../../utils/meta'
import importOverleafModules from '../../../../../macros/import-overleaf-module.macro' import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
import customLocalStorage from '../../../../infrastructure/local-storage' import customLocalStorage from '../../../../infrastructure/local-storage'
import { sendMB } from '../../../../infrastructure/event-tracking' import { sendMB } from '../../../../infrastructure/event-tracking'
import classNames from 'classnames' import classNames from 'classnames'
import BRLBanner from './ads/brl-banner' import GeoBanners from './geo-banners'
type Subscription = { type Subscription = {
groupId: string groupId: string
@ -36,8 +35,6 @@ function UserNotifications() {
'ol-groupSubscriptionsPendingEnrollment', 'ol-groupSubscriptionsPendingEnrollment',
[] []
) )
const showInrGeoBanner = getMeta('ol-showInrGeoBanner', false)
const showBrlGeoBanner = getMeta('ol-showBrlGeoBanner', false)
const user = getMeta('ol-user') const user = getMeta('ol-user')
// Temporary workaround to prevent also showing groups/enterprise banner // Temporary workaround to prevent also showing groups/enterprise banner
@ -85,8 +82,8 @@ function UserNotifications() {
<Institution /> <Institution />
<ConfirmEmail /> <ConfirmEmail />
<ReconfirmationInfo /> <ReconfirmationInfo />
<GeoBanners />
{!showWritefull && !dismissedWritefull && <GroupsAndEnterpriseBanner />} {!showWritefull && !dismissedWritefull && <GroupsAndEnterpriseBanner />}
{showInrGeoBanner && <INRBanner />}
<WritefullPremiumPromoBanner <WritefullPremiumPromoBanner
show={showWritefull} show={showWritefull}
@ -95,7 +92,6 @@ function UserNotifications() {
setDismissedWritefull(true) setDismissedWritefull(true)
}} }}
/> />
{showBrlGeoBanner && <BRLBanner />}
</ul> </ul>
</div> </div>
) )

View file

@ -176,8 +176,6 @@
"bonus_please_recommend_us": "Bonus - Please recommend us", "bonus_please_recommend_us": "Bonus - Please recommend us",
"bonus_share_link_text": "Online LaTeX Editor __appName__", "bonus_share_link_text": "Online LaTeX Editor __appName__",
"bonus_twitter_share_text": "Im using __appName__, the free online collaborative LaTeX editor - its awesome and easy to use!", "bonus_twitter_share_text": "Im using __appName__, the free online collaborative LaTeX editor - its awesome and easy to use!",
"brl_discount_modal_info": "Unlock the full potential of Overleaf with a 50% discount on premium subscriptions paid in Brazilian Reais. Get a longer compile timeout, full document history, track changes, additional collaborators, and more.",
"brl_discount_modal_title": "Premium subscription discount",
"brl_discount_offer_plans_page_banner": "__flag__ <b>Great news!</b> Weve applied a 50% discount to premium plans on this page for our users in Brazil. Check out the new lower prices.", "brl_discount_offer_plans_page_banner": "__flag__ <b>Great news!</b> Weve applied a 50% discount to premium plans on this page for our users in Brazil. Check out the new lower prices.",
"browser": "Browser", "browser": "Browser",
"built_in": "Built-In", "built_in": "Built-In",
@ -972,6 +970,9 @@
"last_updated": "Last Updated", "last_updated": "Last Updated",
"last_updated_date_by_x": "__lastUpdatedDate__ by __person__", "last_updated_date_by_x": "__lastUpdatedDate__ by __person__",
"last_used": "last used", "last_used": "last used",
"latam_discount_modal_info": "Unlock the full potential of Overleaf with a __discount__% discount on premium subscriptions paid in __currencyName__. Get a longer compile timeout, full document history, track changes, additional collaborators, and more.",
"latam_discount_modal_title": "Premium subscription discount",
"latam_discount_offer_plans_page_banner": "__flag__ <b>Great news!</b> Weve applied a __discount__ discount to premium plans on this page for our users in __country__. Check out the new lower prices.",
"latex_articles_page_summary": "Papers, presentations, reports and more, written in LaTeX and published by our community. Search or browse below.", "latex_articles_page_summary": "Papers, presentations, reports and more, written in LaTeX and published by our community. Search or browse below.",
"latex_articles_page_title": "Articles - Papers, Presentations, Reports and more", "latex_articles_page_title": "Articles - Papers, Presentations, Reports and more",
"latex_editor_info": "Everything you need in a modern LaTeX editor --- spell check, intelligent autocomplete, syntax highlighting, dozens of color themes, vim and emacs bindings, help with LaTeX warnings and error messages, and much more.", "latex_editor_info": "Everything you need in a modern LaTeX editor --- spell check, intelligent autocomplete, syntax highlighting, dozens of color themes, vim and emacs bindings, help with LaTeX warnings and error messages, and much more.",

View file

@ -85,8 +85,6 @@
"bonus_please_recommend_us": "Bônus - Por favor nos recomende", "bonus_please_recommend_us": "Bônus - Por favor nos recomende",
"bonus_share_link_text": "Editor Online de LaTeX __appName__", "bonus_share_link_text": "Editor Online de LaTeX __appName__",
"bonus_twitter_share_text": "Estou usando o __appName__, o editor gratuito online e colaborativo de LaTeX - é muito bom e fácil de usar!", "bonus_twitter_share_text": "Estou usando o __appName__, o editor gratuito online e colaborativo de LaTeX - é muito bom e fácil de usar!",
"brl_discount_modal_info": "Obtenha todo o potencial do Overleaf com desconto de 50% em assinaturas premium pagas em reais. Obtenha um tempo limite de compilação mais longo, histórico completo de documentos, controle de alterações, colaboradores adicionais e muito mais.",
"brl_discount_modal_title": "Desconto em assinaturas premium",
"brl_discount_offer_plans_page_banner": "__flag__ Aplicamos um desconto de 50% aos planos premium nesta página para nossos usuários no Brasil. Confira os novos preços mais baixos.", "brl_discount_offer_plans_page_banner": "__flag__ Aplicamos um desconto de 50% aos planos premium nesta página para nossos usuários no Brasil. Confira os novos preços mais baixos.",
"built_in": "Embutido", "built_in": "Embutido",
"bulk_accept_confirm": "Vocês tem certeza que deseja aceitar as __nChanges__ alterações selecionadas?", "bulk_accept_confirm": "Vocês tem certeza que deseja aceitar as __nChanges__ alterações selecionadas?",
@ -327,6 +325,8 @@
"language": "Idioma", "language": "Idioma",
"last_modified": "Última Modificação", "last_modified": "Última Modificação",
"last_name": "Sobrenome", "last_name": "Sobrenome",
"latam_discount_modal_info": "Obtenha todo o potencial do Overleaf com desconto de __discount__% em assinaturas premium pagas em __currencyName__. Obtenha um tempo limite de compilação mais longo, histórico completo de documentos, controle de alterações, colaboradores adicionais e muito mais.",
"latam_discount_modal_title": "Desconto em assinaturas premium",
"latex_editor_info": "Tudo que você precisa num editar moderno de LaTeX - correção ortográfica, auto-completar inteligente, realce de sintaxe, diversas cores de temas, atalhos de vim e emacs, ajuda com mensagens de aviso e erros do LaTeX e muito mais.", "latex_editor_info": "Tudo que você precisa num editar moderno de LaTeX - correção ortográfica, auto-completar inteligente, realce de sintaxe, diversas cores de temas, atalhos de vim e emacs, ajuda com mensagens de aviso e erros do LaTeX e muito mais.",
"latex_templates": "Modelos LaTeX", "latex_templates": "Modelos LaTeX",
"ldap": "LDAP", "ldap": "LDAP",

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB