feat: add translations integration
This commit is contained in:
parent
6626c0fd2f
commit
e644d8873f
@ -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>
|
||||
)
|
||||
|
||||
@ -8,11 +8,10 @@
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
19
src/components/Footer/index.tsx
Normal file
19
src/components/Footer/index.tsx
Normal 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>© {year}, {t('company_name')}</p>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
export default Footer
|
||||
18
src/components/Footer/styles.css
Normal file
18
src/components/Footer/styles.css
Normal 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;
|
||||
}
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
6
src/components/NotFoundPage/styles.css
Normal file
6
src/components/NotFoundPage/styles.css
Normal file
@ -0,0 +1,6 @@
|
||||
.not-found-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>© 20223, Wit LLC, California, US</p>
|
||||
</footer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
)
|
||||
|
||||
@ -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
38
src/locales/dev.ts
Normal 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',
|
||||
},
|
||||
}
|
||||
@ -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',
|
||||
},
|
||||
}
|
||||
@ -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 }
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user