diff --git a/package-lock.json b/package-lock.json index 3bd0c69..ac7d690 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,11 @@ "name": "aurawebapp", "version": "0.0.0", "dependencies": { + "i18next": "^22.4.15", "react": "^18.2.0", "react-circular-progressbar": "^2.1.0", "react-dom": "^18.2.0", + "react-i18next": "^12.2.2", "react-router-dom": "^6.11.0" }, "devDependencies": { @@ -339,6 +341,17 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz", + "integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==", + "dependencies": { + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", @@ -2066,6 +2079,36 @@ "node": ">=4" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/i18next": { + "version": "22.4.15", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.4.15.tgz", + "integrity": "sha512-yYudtbFrrmWKLEhl6jvKUYyYunj4bTBCe2qIUYAxbXoPusY7YmdwPvOE6fx6UIfWvmlbCWDItr7wIs8KEBZ5Zg==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.20.6" + } + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -2573,6 +2616,27 @@ "react": "^18.2.0" } }, + "node_modules/react-i18next": { + "version": "12.2.2", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.2.2.tgz", + "integrity": "sha512-KBB6buBmVKXUWNxXHdnthp+38gPyBT46hJCAIQ8rX19NFL/m2ahte2KARfIDf2tMnSAL7wwck6eDOd/9zn6aFg==", + "dependencies": { + "@babel/runtime": "^7.20.6", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 19.0.0", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", @@ -2612,6 +2676,11 @@ "react-dom": ">=16.8" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2985,6 +3054,14 @@ "node": ">=14.8.0" } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3262,6 +3339,14 @@ "@babel/helper-plugin-utils": "^7.19.0" } }, + "@babel/runtime": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz", + "integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==", + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, "@babel/template": { "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", @@ -4411,6 +4496,22 @@ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true }, + "html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "requires": { + "void-elements": "3.1.0" + } + }, + "i18next": { + "version": "22.4.15", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.4.15.tgz", + "integrity": "sha512-yYudtbFrrmWKLEhl6jvKUYyYunj4bTBCe2qIUYAxbXoPusY7YmdwPvOE6fx6UIfWvmlbCWDItr7wIs8KEBZ5Zg==", + "requires": { + "@babel/runtime": "^7.20.6" + } + }, "ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -4761,6 +4862,15 @@ "scheduler": "^0.23.0" } }, + "react-i18next": { + "version": "12.2.2", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.2.2.tgz", + "integrity": "sha512-KBB6buBmVKXUWNxXHdnthp+38gPyBT46hJCAIQ8rX19NFL/m2ahte2KARfIDf2tMnSAL7wwck6eDOd/9zn6aFg==", + "requires": { + "@babel/runtime": "^7.20.6", + "html-parse-stringify": "^3.0.1" + } + }, "react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", @@ -4784,6 +4894,11 @@ "react-router": "6.11.0" } }, + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + }, "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -5005,6 +5120,11 @@ "fast-glob": "^3.2.7" } }, + "void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==" + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index b5bbe61..fc87edb 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,11 @@ "preview": "vite preview" }, "dependencies": { + "i18next": "^22.4.15", "react": "^18.2.0", "react-circular-progressbar": "^2.1.0", "react-dom": "^18.2.0", + "react-i18next": "^12.2.2", "react-router-dom": "^6.11.0" }, "devDependencies": { diff --git a/src/components/BirthdayPage/index.tsx b/src/components/BirthdayPage/index.tsx index 1271202..a50eac7 100644 --- a/src/components/BirthdayPage/index.tsx +++ b/src/components/BirthdayPage/index.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' +import { useTranslation } from 'react-i18next' import routes from '../../routes' import Policy from '../Policy' import Purposes from '../Purposes' @@ -9,7 +10,8 @@ import MainButton from '../MainButton' import './styles.css' function BirthdayPage(): JSX.Element { - const navigate = useNavigate(); + const { t } = useTranslation() + const navigate = useNavigate() const [birthdate, setBirthdate] = useState('') const [isDisabled, setIsDisabled] = useState(true) const links = [ @@ -29,19 +31,17 @@ function BirthdayPage(): JSX.Element { return (
- Let's start! - What's your date of birth? + {t('letsStart')} + {t('dateOfBirth')} setIsDisabled(true)} /> - +
diff --git a/src/components/BirthtimePage/index.tsx b/src/components/BirthtimePage/index.tsx index 2c30f18..72e131f 100644 --- a/src/components/BirthtimePage/index.tsx +++ b/src/components/BirthtimePage/index.tsx @@ -1,5 +1,6 @@ import { useState, useEffect } from "react" import { useNavigate } from "react-router-dom" +import { useTranslation } from 'react-i18next' import Title from "../Title" import MainButton from "../MainButton" import { TimePicker } from "../DateTimePicker" @@ -7,6 +8,7 @@ import routes from "../../routes" import './styles.css' function BirthtimePage(): JSX.Element { + const { t } = useTranslation() const navigate = useNavigate(); const [birthtime, setBirhtime] = useState('') const [isDisabled, setIsDisabled] = useState(true) @@ -23,12 +25,10 @@ function BirthtimePage(): JSX.Element { return (
- What time were you born? -

- 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. -

+ {t('bornTimeQuestion')} +

{t('nasaDataUsing')}

setBirhtime(value)}/> - +
) } diff --git a/src/components/CallToAction/index.tsx b/src/components/CallToAction/index.tsx index 375c05f..ca62c94 100644 --- a/src/components/CallToAction/index.tsx +++ b/src/components/CallToAction/index.tsx @@ -1,10 +1,12 @@ +import { useTranslation } from 'react-i18next' import './styles.css' function CallToAction(): JSX.Element { + const { t } = useTranslation() return (
-

Start your 7-day trial

-

No pressure. Cancel anytime.

+

{t('ctaTitle')}

+

{t('ctaSubtitle')}

) } diff --git a/src/components/Countdown/index.tsx b/src/components/Countdown/index.tsx index 35b0665..2e34313 100644 --- a/src/components/Countdown/index.tsx +++ b/src/components/Countdown/index.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react' +import { useTranslation } from 'react-i18next' import './styles.css' type CountdownProps = { @@ -6,6 +7,7 @@ type CountdownProps = { } function Countdown({ start }: CountdownProps): JSX.Element { + const { t } = useTranslation() const [time, setTime] = useState(start * 60 - 1) const formatTime = (seconds: number) => { const minutes = Math.floor(seconds / 60) @@ -21,7 +23,7 @@ function Countdown({ start }: CountdownProps): JSX.Element { return (
-

Reserved for {formatTime(time)}

+

{t('reservedFor')}{formatTime(time)}

) } diff --git a/src/components/CreateProfilePage/index.tsx b/src/components/CreateProfilePage/index.tsx index c1ba642..024b4eb 100644 --- a/src/components/CreateProfilePage/index.tsx +++ b/src/components/CreateProfilePage/index.tsx @@ -1,6 +1,7 @@ import { useState } from "react" import { useNavigate } from "react-router-dom" import { CircularProgressbar, buildStyles } from 'react-circular-progressbar' +import { useTranslation } from 'react-i18next' import ProcessFlow from "./ProcessFlow" import Title from "../Title" import routes from "../../routes" @@ -9,12 +10,13 @@ import './styles.css' const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) function CreateProfilePage(): JSX.Element { + const { t } = useTranslation() const navigate = useNavigate() const [progress, setProgress] = useState(0) const processItems = [ - { task: () => sleep(3300).then(() => setProgress(35)), label: 'Zodiac data analysis' }, - { task: () => sleep(2550).then(() => setProgress(61)), label: 'Drawing Wallpapers' }, - { task: () => sleep(3789).then(() => setProgress(98)), label: 'Preparing results' }, + { 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') }, ] const handleDone = () => Promise.resolve() .then(() => setProgress(100)) @@ -23,7 +25,7 @@ function CreateProfilePage(): JSX.Element { return (
- Creating your profile + {t('creatingProfile')}
): JSX.Element { + const { t } = useTranslation() const { name, value, onValid, onInvalid } = props const date = new Date(value) @@ -35,7 +37,7 @@ export function DatePicker(props: FormField): JSX.Element { value={year} max={getMaxYear()} maxLength={4} - label='Year' + label={t('year')} placeholder='YYYY' onChange={(year: number) => setYear(year)} /> @@ -44,7 +46,7 @@ export function DatePicker(props: FormField): JSX.Element { value={month} max={12} maxLength={2} - label='Month' + label={t('month')} placeholder='MM' onChange={(month: number) => setMonth(month)} /> @@ -53,14 +55,14 @@ export function DatePicker(props: FormField): JSX.Element { value={day} max={getDaysInMonth(year, month)} maxLength={2} - label='Day' + label={t('day')} placeholder='DD' onChange={(day: number) => setDay(day)} />
) diff --git a/src/components/EmailEnterPage/index.tsx b/src/components/EmailEnterPage/index.tsx index f328dc2..fdc0dd2 100644 --- a/src/components/EmailEnterPage/index.tsx +++ b/src/components/EmailEnterPage/index.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' +import { useTranslation } from 'react-i18next' import Title from '../Title' import Policy from '../Policy' import EmailInput from '../EmailInput' @@ -7,6 +8,7 @@ import MainButton from '../MainButton' import routes from '../../routes' function EmailEnterPage(): JSX.Element { + const { t } = useTranslation() const navigate = useNavigate() const [email, setEmail] = useState('') const [isDisabled, setIsDisabled] = useState(true) @@ -26,21 +28,17 @@ function EmailEnterPage(): JSX.Element { return (
- - We will email you a copy of your wallpaper for easy access. - + {t('weWillEmailYou')} setIsDisabled(true)} /> -

We don't share any personal information.

- - By clicking "Continue" below, you agree to our EULA and Privacy Policy. - - +

{t('weDontShare')}

+ {t('continueAgree')} +
) } diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx index e92ca7c..258fd64 100644 --- a/src/components/Header/index.tsx +++ b/src/components/Header/index.tsx @@ -1,11 +1,13 @@ import { useState, useEffect } from 'react' import { useNavigate, useLocation } from 'react-router-dom' +import { useTranslation } from 'react-i18next' import routes, { isNotEntrypoint } from '../../routes' import BackButton from '../BackButton' import iconUrl from './icon.png' import './styles.css' function Header(): JSX.Element { + const { t } = useTranslation() const navigate = useNavigate() const location = useLocation() const [initialPath, setInitialPath] = useState(null); @@ -33,7 +35,7 @@ function Header(): JSX.Element {
{ showBackButton ? : null } logo - Aura + {t('appName')}
) } diff --git a/src/components/NotFoundPage/index.tsx b/src/components/NotFoundPage/index.tsx index 843fed0..e488e4d 100644 --- a/src/components/NotFoundPage/index.tsx +++ b/src/components/NotFoundPage/index.tsx @@ -1,8 +1,11 @@ +import { useTranslation } from 'react-i18next' + function NotFoundPage() { + const { t } = useTranslation() return ( <> -

Oops!

-

Sorry, an unexpected error has occurred.

+

{t('oops')}

+

{t('unexpectedError')}

) } diff --git a/src/components/Payment/index.tsx b/src/components/Payment/index.tsx index bb7e866..1d5c9a8 100644 --- a/src/components/Payment/index.tsx +++ b/src/components/Payment/index.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next' import Price, { Currency, Locale } from './Price' import './styles.css' @@ -14,6 +15,7 @@ type PaymentProps = { } function Payment({ currency, locale, items }: PaymentProps): JSX.Element { + const { t } = useTranslation() const total = items.reduce((acc, item) => acc + item.price, 0) const totalPrice = new Price(total, currency, locale) const toItem = (item: typeof items[0], idx: number) => { @@ -35,16 +37,14 @@ function Payment({ currency, locale, items }: PaymentProps): JSX.Element {
-
Total today
+
{t('totalToday')}
{totalPrice.format()}
{items.map(toItem)}
-
- You will be charged only $1 for your 7-day trial. We'll email you a reminder before your trial period ends. Cancel anytime. -
+
{t('chargedOnly')}
) } diff --git a/src/components/Purposes/index.tsx b/src/components/Purposes/index.tsx index 9aaf069..0612e7b 100644 --- a/src/components/Purposes/index.tsx +++ b/src/components/Purposes/index.tsx @@ -1,7 +1,9 @@ +import { useTranslation } from 'react-i18next' import './styles.css' function Purposes(): JSX.Element { - return For entertaiment purposes only + const { t } = useTranslation() + return {t('purposes')} } export default Purposes diff --git a/src/components/SubscriptionPage/index.tsx b/src/components/SubscriptionPage/index.tsx index d86a773..a324e71 100644 --- a/src/components/SubscriptionPage/index.tsx +++ b/src/components/SubscriptionPage/index.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next' import MainButton from '../MainButton' import Policy from '../Policy' import Countdown from '../Countdown' @@ -6,6 +7,7 @@ import UserHeader from '../UserHeader' import CallToAction from '../CallToAction' function SubscriptionPage(): JSX.Element { + const { t } = useTranslation() const userEmail = 'some@email.com' const links = [ { text: 'Subscription policy', href: 'https://aura.wit.life/' }, @@ -27,10 +29,8 @@ function SubscriptionPage(): JSX.Element { - - - 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 - + + {t('subscriptionPolicy')}
) diff --git a/src/init.tsx b/src/init.tsx index 771abb7..d97ba8d 100644 --- a/src/init.tsx +++ b/src/init.tsx @@ -1,13 +1,26 @@ import React from 'react' import { BrowserRouter } from 'react-router-dom' +import i18next from 'i18next' +import { I18nextProvider, initReactI18next } from 'react-i18next' +import resources from './locales' +import routes from './routes' import App from './components/App' const init = async () => { + const response = await fetch(routes.server.translations()) + const data = await response.json() + const defaultLanguage = data.meta.locale + // TODO: add translations from data.translations + const i18nextInstance = i18next.createInstance() + const options = { lng: defaultLanguage, resources } + await i18nextInstance.use(initReactI18next).init(options) return ( - - - + + + + + ) } diff --git a/src/locales/en.ts b/src/locales/en.ts new file mode 100644 index 0000000..dd7a3ed --- /dev/null +++ b/src/locales/en.ts @@ -0,0 +1,34 @@ +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', + }, +} diff --git a/src/locales/index.ts b/src/locales/index.ts new file mode 100644 index 0000000..56f1662 --- /dev/null +++ b/src/locales/index.ts @@ -0,0 +1,3 @@ +import en from './en.ts' + +export default { en } diff --git a/src/routes.ts b/src/routes.ts index 12ab951..a2e4e93 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -1,5 +1,6 @@ -const host = ''; -const prefix = 'api/v1'; +const host = '' +const apiHost = 'https://aura.wit.life' +const prefix = 'api/v1' const routes = { client: { @@ -11,9 +12,9 @@ const routes = { createProfile: () => [host, 'profile', 'create'].join('/'), }, server: { - locales: () => [host, prefix, 'locales.json'].join('/'), - translations: () => [host, prefix, 't.json'].join('/'), - userRegistration: () => [host, prefix, 'user', 'registration.json'].join('/'), + locales: () => [apiHost, prefix, 'locales.json'].join('/'), + translations: () => [apiHost, prefix, 't.json'].join('/'), + userRegistration: () => [apiHost, prefix, 'user', 'registration.json'].join('/'), }, } diff --git a/src/types.ts b/src/types.ts index c9ec3cf..2ecb854 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,8 +1,8 @@ export interface FormField { name: string value: T - label?: string - placeholder?: string + label?: string | null + placeholder?: string | null onValid: (value: T) => void onInvalid: () => void }