feat: add translations integration

This commit is contained in:
Aidar Shaikhutdin @makeweb.space 2023-05-20 07:01:54 +06:00
parent 6626c0fd2f
commit e644d8873f
22 changed files with 133 additions and 82 deletions

View File

@ -11,6 +11,7 @@ import WallpaperPage from '../WallpaperPage'
import NotFoundPage from '../NotFoundPage'
import Header from '../Header'
import Navbar from '../Navbar'
import Footer from '../Footer'
import routes, { hasNavigation } from '../../routes'
import './styles.css'
@ -44,6 +45,7 @@ function Layout(): JSX.Element {
<div className='container'>
<Header openMenu={() => setIsMenuOpen(true)}/>
<main className='content'><Outlet /></main>
<Footer color={showNavbar ? 'black' : 'white'}/>
{ showNavbar ? <Navbar isOpen={isMenuOpen} closeMenu={() => setIsMenuOpen(false)} /> : null}
</div>
)

View File

@ -8,11 +8,10 @@
}
.content {
width: 100%;
height: 100vh;
position: relative;
display: flex;
flex-direction: column;
position: relative;
width: 100%;
}
.page {

View File

@ -30,8 +30,8 @@ function BirthdayPage(): JSX.Element {
return (
<section className='page'>
<Title variant='h3' className='mt-24'>{t('letsStart')}</Title>
<Title variant='h2'>{t('dateOfBirth')}</Title>
<Title variant='h3' className='mt-24'>{t('lets_start')}</Title>
<Title variant='h2'>{t('date_of_birth')}</Title>
<DatePicker
name='birthdate'
value={birthdate}
@ -42,7 +42,7 @@ function BirthdayPage(): JSX.Element {
{t('next')}
</MainButton>
<footer className='footer'>
<Policy links={links}>{t('privacyText')}</Policy>
<Policy links={links}>{t('privacy_text')}</Policy>
<Purposes />
</footer>
</section>

View File

@ -17,8 +17,8 @@ function BirthtimePage(): JSX.Element {
const handleChange = (value: string) => dispatch(actions.form.addTime(value))
return (
<section className='page'>
<Title variant="h2" className="mt-24">{t('bornTimeQuestion')}</Title>
<p className="description">{t('nasaDataUsing')}</p>
<Title variant="h2" className="mt-24">{t('born_time_question')}</Title>
<p className="description">{t('nasa_data_using')}</p>
<TimePicker value={birthtime} onChange={handleChange}/>
<MainButton onClick={handleNext}>{t('next')}</MainButton>
</section>

View File

@ -5,8 +5,8 @@ function CallToAction(): JSX.Element {
const { t } = useTranslation()
return (
<div className='call-to-action mb-24'>
<h1>{t('ctaTitle')}</h1>
<p>{t('ctaSubtitle')}</p>
<h1>{t('cta_title')}</h1>
<p>{t('cta_subtitle')}</p>
</div>
)
}

View File

@ -23,7 +23,7 @@ function Countdown({ start }: CountdownProps): JSX.Element {
return (
<div className="countdown mb-24">
<p>{t('reservedFor')}{formatTime(time)}</p>
<p>{t('reserved_for')}{formatTime(time)}</p>
</div>
)
}

View File

@ -14,9 +14,9 @@ function CreateProfilePage(): JSX.Element {
const navigate = useNavigate()
const [progress, setProgress] = useState(0)
const processItems = [
{ task: () => sleep(3300).then(() => setProgress(35)), label: t('zodiacAnalysis') },
{ task: () => sleep(2550).then(() => setProgress(61)), label: t('drawingWallpaper') },
{ task: () => sleep(3789).then(() => setProgress(98)), label: t('preparingResults') },
{ task: () => sleep(3300).then(() => setProgress(35)), label: t('zodiac_analysis') },
{ task: () => sleep(2550).then(() => setProgress(61)), label: t('drawing_wallpaper') },
{ task: () => sleep(3789).then(() => setProgress(98)), label: t('preparing_results') },
]
const handleDone = () => Promise.resolve()
.then(() => setProgress(100))
@ -25,7 +25,7 @@ function CreateProfilePage(): JSX.Element {
return (
<section className='page'>
<Title variant="h2" className="mt-24">{t('creatingProfile')}</Title>
<Title variant="h2" className="mt-24">{t('creating_profile')}</Title>
<div className="progressbar">
<CircularProgressbar
value={progress}

View File

@ -63,7 +63,7 @@ export function DatePicker(props: FormField<string>): JSX.Element {
onChange={(day: string) => setDay(day)}
/>
</div>
<ErrorText isShown={hasError} message={t('invalidDate')} />
<ErrorText isShown={hasError} message={t('invalid_date')} />
</form>
)
}

View File

@ -57,16 +57,16 @@ function EmailEnterPage(): JSX.Element {
return (
<section className='page'>
<Title variant='h2' className='mt-24'>{t('weWillEmailYou')}</Title>
<Title variant='h2' className='mt-24'>{t('we_will_email_you')}</Title>
<EmailInput
name="email"
value={email}
placeholder={t('yourEmail')}
placeholder={t('your_email')}
onValid={handleValidEmail}
onInvalid={() => setIsDisabled(true)}
/>
<p>{t('weDontShare')}</p>
<Policy links={links} sizing='medium'>{t('continueAgree')}</Policy>
<p>{t('we_dont_share')}</p>
<Policy links={links} sizing='medium'>{t('continue_agree')}</Policy>
<MainButton onClick={handleClick} disabled={isDisabled}>
{isLoading ? <Loader color={LoaderColor.White} /> : t('continue')}
</MainButton>

View File

@ -0,0 +1,19 @@
import { useTranslation } from 'react-i18next'
import './styles.css'
type FooterProps = {
color?: 'white' | 'black'
}
function Footer({ color = 'white' }: FooterProps): JSX.Element {
const { t } = useTranslation()
const year = new Date().getFullYear()
const combinedClassNames = ['page-footer', `page-footer--${color}`].filter(Boolean).join(' ')
return (
<footer className={combinedClassNames}>
<p>&copy; {year}, {t('company_name')}</p>
</footer>
)
}
export default Footer

View File

@ -0,0 +1,18 @@
.page-footer {
font-size: 14px;
font-weight: 400;
line-height: 2;
padding: 10px 0;
color: #8e8e93;
text-align: center;
}
.page-footer--white {
background-color: #fff;
}
.page-footer--black {
background-color: #04040a;
color: #fff;
}

View File

@ -41,7 +41,7 @@ function Header({ openMenu }: HeaderProps): JSX.Element {
<header className="header">
{ showBackButton ? <BackButton className="pa" onClick={goBack} /> : null }
<img src={iconUrl} alt="logo" width="40" height="40" />
<span className="header__title">{t('appName')}</span>
<span className="header__title">{t('app_name')}</span>
{showMenuButton ? <div className="header__menu-btn" onClick={openMenu}>
<img src={menuUrl} alt="menu" width="40" height="40" />
</div> : null}

View File

@ -3,10 +3,10 @@ import { useTranslation } from 'react-i18next'
function NotFoundPage() {
const { t } = useTranslation()
return (
<>
<div className='not-found-page'>
<h1>{t('oops')}</h1>
<p>{t('unexpectedError')}</p>
</>
<p>{t('unexpected_error')}</p>
</div>
)
}

View File

@ -0,0 +1,6 @@
.not-found-page {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}

View File

@ -37,14 +37,14 @@ function Payment({ currency, locale, items }: PaymentProps): JSX.Element {
<div className='payment'>
<div className='payment__table'>
<div className='payment__total'>
<div className='payment__total-title'>{t('totalToday')}</div>
<div className='payment__total-title'>{t('total_today')}</div>
<div className='payment__total-price'>{totalPrice.format()}</div>
</div>
<div className='payment__items'>
{items.map(toItem)}
</div>
</div>
<div className='payment__information'>{t('chargedOnly')}</div>
<div className='payment__information'>{t('charged_only')}</div>
</div>
)
}

View File

@ -24,7 +24,7 @@ const isApple = () => /Macintosh|iPhone|iPad|iPod/i.test(navigator.userAgent)
function PaymentPage(): JSX.Element {
const api = useApi()
const { token } = useAuth()
const { i18n } = useTranslation()
const { t, i18n } = useTranslation()
const locale = i18n.language
const navigate = useNavigate()
const email = useSelector(selectors.selectEmail)
@ -45,25 +45,22 @@ function PaymentPage(): JSX.Element {
{isApple() && <img src={applePaySafeCheckout} alt='Guaranteed safe checkout' />}
<img src={secure} alt='100% Secure' />
</div>
<Title variant='h1' className='mb-45'>Choose Payment Method</Title>
<Title variant='h1' className='mb-45'>{t('choose_payment')}</Title>
{isPending ? <Loader /> : (
<>
<MainButton onClick={handleClick}>
{isAndroid() && <img className='payment-btn' src={GooglePay} alt='Google Pay' />}
{isApple() && <img className='payment-btn' src={ApplePay} alt='Apple Pay' />}
</MainButton>
<div className='payment-divider'>OR</div>
<div className='payment-divider'>{t('or').toUpperCase()}</div>
<MainButton color='blue' onClick={handleClick}>
<img className='payment-card' src={card} alt='Credit / Debit Card' />
Credit / Debit Card
{t('card')}
</MainButton>
<p className='payment-warining'>You will be charged only <strong>$1 for your 7-day trial</strong>. We'll email your a reminder before your trial period ends.</p>
</>
)}
</section>
<footer className='page-footer'>
<p>&copy; 20223, Wit LLC, California, US</p>
</footer>
</>
)
}

View File

@ -37,12 +37,3 @@
line-height: 1.5;
font-size: 15px;
}
.page-footer {
font-size: 14px;
font-weight: 400;
line-height: 2;
padding: 10px 0;
color: #8e8e93;
text-align: center;
}

View File

@ -34,8 +34,8 @@ function SubscriptionPage(): JSX.Element {
<CallToAction />
<Countdown start={10}/>
<Payment items={paymentItems} currency={currency} locale={locale}/>
<MainButton onClick={handleClick}>{t('getAccess')}</MainButton>
<Policy links={links}>{t('subscriptionPolicy')}</Policy>
<MainButton onClick={handleClick}>{t('get_access')}</MainButton>
<Policy links={links}>{t('subscription_policy')}</Policy>
</section>
</>
)

View File

@ -6,14 +6,15 @@ import { Provider } from 'react-redux'
import { store } from './store'
import { AuthProvider } from './auth'
import { ApiContext, createApi } from './api'
import resources, { getClientLocale } from './locales'
import { getClientLocale, buildResources, fallbackLng } from './locales'
import App from './components/App'
const init = async () => {
const api = createApi()
const lng = getClientLocale()
const resources = await api.getElements({ locale: lng }).then(buildResources)
const i18nextInstance = i18next.createInstance()
const options = { lng, resources }
const options = { lng, resources, fallbackLng }
await i18nextInstance.use(initReactI18next).init(options)
return (
<React.StrictMode>

38
src/locales/dev.ts Normal file
View File

@ -0,0 +1,38 @@
export default {
translation: {
lets_start: "Let's start!",
next: "Next",
date_of_birth: "What's your date of birth?",
privacy_text: "By continuing, you agree to our EULA and Privacy Notice. Have a question? Reach our support team here",
born_time_question: "What time were you born?",
nasa_data_using: "We use NASA data to determine the exact position of the planets in the sky at the time of your birth to create wallpapers that are just right for you.",
cta_title: "Start your 7-day trial",
cta_subtitle: "No pressure. Cancel anytime.",
reserved_for: "Reserved for ",
creating_profile: "Creating your profile",
zodiac_analysis: "Zodiac data analysis",
drawing_wallpaper: "Drawing Wallpapers",
preparing_results: "Preparing results",
invalid_date: "Date not found. Please check your details and try again.",
year: "Year",
month: "Month",
day: "Day",
we_will_email_you: "We will email you a copy of your wallpaper for easy access.",
your_email: "Your email",
we_dont_share: "We don't share any personal information.",
continue_agree: 'By clicking "Continue" below, you agree to our EULA and Privacy Policy.',
continue: 'Continue',
app_name: "Aura",
unexpected_error: 'Sorry, an unexpected error has occurred.',
oops: "Oops!",
total_today: 'Total today',
charged_only: "You will be charged only $1 for your 7-day trial. We'll email you a reminder before your trial period ends. Cancel anytime.",
purposes: 'For entertaiment purposes only.',
get_access: 'Get access',
subscription_policy: 'By proceeding, you agree that if you do not cancel your subscription before the end of the 7-day trial period, you will be automatically charged nineteen US dollars zero cents every 2 weeks until you cancel the subscription in the settings. Learn more about cancellation and refund policy in Subscription policy',
company_name: 'Wit LLC, California, US',
choose_payment: 'Choose Payment Method',
or: 'OR',
card: 'Credit / Debit Card',
},
}

View File

@ -1,34 +0,0 @@
export default {
translation: {
letsStart: "Let's start!",
next: "Next",
dateOfBirth: "What's your date of birth?",
privacyText: "By continuing, you agree to our EULA and Privacy Notice. Have a question? Reach our support team here",
bornTimeQuestion: "What time were you born?",
nasaDataUsing: "We use NASA data to determine the exact position of the planets in the sky at the time of your birth to create wallpapers that are just right for you.",
ctaTitle: "Start your 7-day trial",
ctaSubtitle: "No pressure. Cancel anytime.",
reservedFor: "Reserved for ",
creatingProfile: "Creating your profile",
zodiacAnalysis: "Zodiac data analysis",
drawingWallpaper: "Drawing Wallpapers",
preparingResults: "Preparing results",
invalidDate: "Date not found. Please check your details and try again.",
year: "Year",
month: "Month",
day: "Day",
weWillEmailYou: "We will email you a copy of your wallpaper for easy access.",
yourEmail: "Your email",
weDontShare: "We don't share any personal information.",
continueAgree: 'By clicking "Continue" below, you agree to our EULA and Privacy Policy.',
continue: 'Continue',
appName: "Aura",
unexpectedError: 'Sorry, an unexpected error has occurred.',
oops: "Oops!",
totalToday: 'Total today',
chargedOnly: "You will be charged only $1 for your 7-day trial. We'll email you a reminder before your trial period ends. Cancel anytime.",
purposes: 'For entertaiment purposes only.',
getAccess: 'Get access',
subscriptionPolicy: 'By proceeding, you agree that if you do not cancel your subscription before the end of the 7-day trial period, you will be automatically charged nineteen US dollars zero cents every 2 weeks until you cancel the subscription in the settings. Learn more about cancellation and refund policy in Subscription policy',
},
}

View File

@ -1,6 +1,20 @@
import en from './en.ts'
import { Elements } from '../api'
import dev from './dev.ts'
export const getClientLocale = () => navigator.language.split('-')[0]
export const getClientTimezone = () => Intl.DateTimeFormat().resolvedOptions().timeZone
export const fallbackLng = 'dev'
export default { en }
const omitKeys = ['href', 'title', 'url_slug', 'type']
const isWeb = (group: Elements.ElementGroup) => group.name === 'web'
const cleanUp = (element: Partial<Elements.ElementGroupItem> = {}) => {
return Object.entries(element)
.filter(([key]) => !omitKeys.includes(key))
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {})
}
export const buildResources = (resp: Elements.Response) => {
const element = resp.data.groups.find(isWeb)?.items.at(0)
const translation = cleanUp(element)
const lng = getClientLocale()
return { [lng]: { translation }, dev }
}