diff --git a/.gitignore b/.gitignore index 93a3f965..1d6378da 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ ui/.env.production ui/extension/dist ui/extension/packages ui/extension/.env +ui/wp-plugin.zip # netstated globe web client netstate/d/webclients/globe/node_modules diff --git a/README.md b/README.md index 39286c5a..e1c41268 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,7 @@ Production: 3. Set `REACT_APP_STORAGE_URL` to your intended iframe html for local storage of widget state and analytics. Most likely `https://embed.lantern.io/storage.html` or `/storage.html` if testing locally 4. Set any `REACT_APP_*` variables as needed for your development environment. See [UI settings and configuration](#ui-settings-and-configuration) for more info. 5. Configure the WASM client endpoints: `REACT_APP_DISCOVERY_SRV`, `REACT_APP_DISCOVERY_ENDPOINT`, `REACT_APP_EGRESS_ADDR` & `REACT_APP_EGRESS_ENDPOINT` + 6. Configure the CMS and translations: `STRAPI_API_TOKEN` & `STRAPI_API_URL` 3. Install the dependencies: `yarn` diff --git a/ui/.env.development.example b/ui/.env.development.example index 20e6a23f..c04ba799 100644 --- a/ui/.env.development.example +++ b/ui/.env.development.example @@ -41,3 +41,7 @@ REACT_APP_EGRESS_ENDPOINT=/ws # use http://localhost:8080/exec for local netstated # https://netstated-d7bbec1ed55b.herokuapp.com/exec for production netstated REACT_APP_NETSTATED_URL=https://netstated-d7bbec1ed55b.herokuapp.com/exec + +# CMS and Translations +STRAPI_API_TOKEN= +STRAPI_API_URL=https://cms.lantern.io/api \ No newline at end of file diff --git a/ui/.env.production.example b/ui/.env.production.example index 0cb6419d..a59dda69 100644 --- a/ui/.env.production.example +++ b/ui/.env.production.example @@ -41,3 +41,7 @@ REACT_APP_EGRESS_ENDPOINT=/ws # use http://localhost:8080/exec for local netstated # https://netstated-d7bbec1ed55b.herokuapp.com/exec for production netstated REACT_APP_NETSTATED_URL=https://netstated-d7bbec1ed55b.herokuapp.com/exec + +# CMS and Translations +STRAPI_API_TOKEN= +STRAPI_API_URL=https://cms.lantern.io/api \ No newline at end of file diff --git a/ui/package.json b/ui/package.json index 433cf5c6..990c6bb7 100644 --- a/ui/package.json +++ b/ui/package.json @@ -15,8 +15,12 @@ "@types/react-dom": "^18.0.6", "@types/styled-components": "^5.1.26", "@webextension-toolbox/webextension-toolbox": "^5.2.2", + "axios": "^1.7.7", "dotenv": "^16.0.3", "gh-pages": "^4.0.0", + "i18next": "^23.15.2", + "i18next-browser-languagedetector": "^8.0.0", + "i18next-http-backend": "^2.6.2", "lottie-react": "^2.4.0", "mini-css-extract-plugin": "^2.6.1", "patch-package": "^6.5.0", @@ -24,6 +28,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-globe.gl": "^2.22.4", + "react-i18next": "^15.0.2", "react-scripts": "5.0.1", "react-tiny-popover": "^7.2.0", "rewire": "^6.0.0", diff --git a/ui/public/locales/ar/translation.json b/ui/public/locales/ar/translation.json new file mode 100644 index 00000000..bc361032 --- /dev/null +++ b/ui/public/locales/ar/translation.json @@ -0,0 +1,24 @@ +{ + "intro": "Join our network of digital volunteers and help unblock the internet around the world.", + "createdAt": "2024-10-10T11:03:23.027Z", + "updatedAt": "2024-10-10T18:13:49.982Z", + "locale": "en", + "title": "Use your browser to fight global internet censorship", + "status": "Status:", + "keep": "Keep this site open to continue sharing your connection.", + "on": "ON", + "off": "OFF", + "now": "People you're helping right now:", + "total": "Total people helped to date:", + "donate": "Donate to Lantern", + "learn": "Learn More", + "peers": "{{count}} people from {{country}}", + "installChrome": "Install on Chrome", + "installFirefox": "Install on Firefox", + "installCta": "Help even more people by installing the extension", + "waiting": "Waiting for connections", + "installCtaBtnLg": "Get Browsers Unbounded for {{browser}}", + "installCtaBtnSm": "Add to {{browser}}", + "peer": "{{count}} person from {{country}}", + "helping": "Helping a new person in {{country}}" +} \ No newline at end of file diff --git a/ui/public/locales/en-US/translation.json b/ui/public/locales/en-US/translation.json new file mode 100644 index 00000000..bc361032 --- /dev/null +++ b/ui/public/locales/en-US/translation.json @@ -0,0 +1,24 @@ +{ + "intro": "Join our network of digital volunteers and help unblock the internet around the world.", + "createdAt": "2024-10-10T11:03:23.027Z", + "updatedAt": "2024-10-10T18:13:49.982Z", + "locale": "en", + "title": "Use your browser to fight global internet censorship", + "status": "Status:", + "keep": "Keep this site open to continue sharing your connection.", + "on": "ON", + "off": "OFF", + "now": "People you're helping right now:", + "total": "Total people helped to date:", + "donate": "Donate to Lantern", + "learn": "Learn More", + "peers": "{{count}} people from {{country}}", + "installChrome": "Install on Chrome", + "installFirefox": "Install on Firefox", + "installCta": "Help even more people by installing the extension", + "waiting": "Waiting for connections", + "installCtaBtnLg": "Get Browsers Unbounded for {{browser}}", + "installCtaBtnSm": "Add to {{browser}}", + "peer": "{{count}} person from {{country}}", + "helping": "Helping a new person in {{country}}" +} \ No newline at end of file diff --git a/ui/public/locales/en/translation.json b/ui/public/locales/en/translation.json new file mode 100644 index 00000000..bc361032 --- /dev/null +++ b/ui/public/locales/en/translation.json @@ -0,0 +1,24 @@ +{ + "intro": "Join our network of digital volunteers and help unblock the internet around the world.", + "createdAt": "2024-10-10T11:03:23.027Z", + "updatedAt": "2024-10-10T18:13:49.982Z", + "locale": "en", + "title": "Use your browser to fight global internet censorship", + "status": "Status:", + "keep": "Keep this site open to continue sharing your connection.", + "on": "ON", + "off": "OFF", + "now": "People you're helping right now:", + "total": "Total people helped to date:", + "donate": "Donate to Lantern", + "learn": "Learn More", + "peers": "{{count}} people from {{country}}", + "installChrome": "Install on Chrome", + "installFirefox": "Install on Firefox", + "installCta": "Help even more people by installing the extension", + "waiting": "Waiting for connections", + "installCtaBtnLg": "Get Browsers Unbounded for {{browser}}", + "installCtaBtnSm": "Add to {{browser}}", + "peer": "{{count}} person from {{country}}", + "helping": "Helping a new person in {{country}}" +} \ No newline at end of file diff --git a/ui/public/locales/es-419/translation.json b/ui/public/locales/es-419/translation.json new file mode 100644 index 00000000..bc361032 --- /dev/null +++ b/ui/public/locales/es-419/translation.json @@ -0,0 +1,24 @@ +{ + "intro": "Join our network of digital volunteers and help unblock the internet around the world.", + "createdAt": "2024-10-10T11:03:23.027Z", + "updatedAt": "2024-10-10T18:13:49.982Z", + "locale": "en", + "title": "Use your browser to fight global internet censorship", + "status": "Status:", + "keep": "Keep this site open to continue sharing your connection.", + "on": "ON", + "off": "OFF", + "now": "People you're helping right now:", + "total": "Total people helped to date:", + "donate": "Donate to Lantern", + "learn": "Learn More", + "peers": "{{count}} people from {{country}}", + "installChrome": "Install on Chrome", + "installFirefox": "Install on Firefox", + "installCta": "Help even more people by installing the extension", + "waiting": "Waiting for connections", + "installCtaBtnLg": "Get Browsers Unbounded for {{browser}}", + "installCtaBtnSm": "Add to {{browser}}", + "peer": "{{count}} person from {{country}}", + "helping": "Helping a new person in {{country}}" +} \ No newline at end of file diff --git a/ui/public/locales/fa/translation.json b/ui/public/locales/fa/translation.json new file mode 100644 index 00000000..bc361032 --- /dev/null +++ b/ui/public/locales/fa/translation.json @@ -0,0 +1,24 @@ +{ + "intro": "Join our network of digital volunteers and help unblock the internet around the world.", + "createdAt": "2024-10-10T11:03:23.027Z", + "updatedAt": "2024-10-10T18:13:49.982Z", + "locale": "en", + "title": "Use your browser to fight global internet censorship", + "status": "Status:", + "keep": "Keep this site open to continue sharing your connection.", + "on": "ON", + "off": "OFF", + "now": "People you're helping right now:", + "total": "Total people helped to date:", + "donate": "Donate to Lantern", + "learn": "Learn More", + "peers": "{{count}} people from {{country}}", + "installChrome": "Install on Chrome", + "installFirefox": "Install on Firefox", + "installCta": "Help even more people by installing the extension", + "waiting": "Waiting for connections", + "installCtaBtnLg": "Get Browsers Unbounded for {{browser}}", + "installCtaBtnSm": "Add to {{browser}}", + "peer": "{{count}} person from {{country}}", + "helping": "Helping a new person in {{country}}" +} \ No newline at end of file diff --git a/ui/public/locales/ru/translation.json b/ui/public/locales/ru/translation.json new file mode 100644 index 00000000..057ba623 --- /dev/null +++ b/ui/public/locales/ru/translation.json @@ -0,0 +1,24 @@ +{ + "intro": "Присоединяйтесь к нашей сети цифровых волонтеров и помогайте разблокировать интернет по всему миру.", + "createdAt": "2024-10-10T11:51:26.104Z", + "updatedAt": "2024-10-10T18:14:28.151Z", + "locale": "ru", + "title": "Используйте свой браузер для борьбы с глобальной интернет-цензурой", + "status": "Статус:", + "keep": "Оставьте этот сайт открытым, чтобы продолжать делиться вашим подключением.", + "on": "ВКЛ", + "off": "ВЫКЛ", + "now": "Люди, которым вы помогаете прямо сейчас:", + "total": "Всего людей, которым помогли на сегодняшний день:", + "donate": "Пожертвовать Lantern", + "learn": "Узнать больше", + "peers": "{{count}} человек из {{country}}", + "installChrome": "Установить в Chrome", + "installFirefox": "Установить в Firefox", + "installCta": "Помогите ещё большему количеству людей, установив расширение", + "waiting": "Ожидание подключений", + "installCtaBtnLg": "Получите Browsers Unbounded для {{browser}}", + "installCtaBtnSm": "Добавить в {{browser}}", + "peer": "{{count}} человек из {{country}}", + "helping": "Помощь новому человеку в {{country}}" +} \ No newline at end of file diff --git a/ui/public/locales/zh-Hans/translation.json b/ui/public/locales/zh-Hans/translation.json new file mode 100644 index 00000000..bc361032 --- /dev/null +++ b/ui/public/locales/zh-Hans/translation.json @@ -0,0 +1,24 @@ +{ + "intro": "Join our network of digital volunteers and help unblock the internet around the world.", + "createdAt": "2024-10-10T11:03:23.027Z", + "updatedAt": "2024-10-10T18:13:49.982Z", + "locale": "en", + "title": "Use your browser to fight global internet censorship", + "status": "Status:", + "keep": "Keep this site open to continue sharing your connection.", + "on": "ON", + "off": "OFF", + "now": "People you're helping right now:", + "total": "Total people helped to date:", + "donate": "Donate to Lantern", + "learn": "Learn More", + "peers": "{{count}} people from {{country}}", + "installChrome": "Install on Chrome", + "installFirefox": "Install on Firefox", + "installCta": "Help even more people by installing the extension", + "waiting": "Waiting for connections", + "installCtaBtnLg": "Get Browsers Unbounded for {{browser}}", + "installCtaBtnSm": "Add to {{browser}}", + "peer": "{{count}} person from {{country}}", + "helping": "Helping a new person in {{country}}" +} \ No newline at end of file diff --git a/ui/public/locales/zh-Hant-TW/translation.json b/ui/public/locales/zh-Hant-TW/translation.json new file mode 100644 index 00000000..bc361032 --- /dev/null +++ b/ui/public/locales/zh-Hant-TW/translation.json @@ -0,0 +1,24 @@ +{ + "intro": "Join our network of digital volunteers and help unblock the internet around the world.", + "createdAt": "2024-10-10T11:03:23.027Z", + "updatedAt": "2024-10-10T18:13:49.982Z", + "locale": "en", + "title": "Use your browser to fight global internet censorship", + "status": "Status:", + "keep": "Keep this site open to continue sharing your connection.", + "on": "ON", + "off": "OFF", + "now": "People you're helping right now:", + "total": "Total people helped to date:", + "donate": "Donate to Lantern", + "learn": "Learn More", + "peers": "{{count}} people from {{country}}", + "installChrome": "Install on Chrome", + "installFirefox": "Install on Firefox", + "installCta": "Help even more people by installing the extension", + "waiting": "Waiting for connections", + "installCtaBtnLg": "Get Browsers Unbounded for {{browser}}", + "installCtaBtnSm": "Add to {{browser}}", + "peer": "{{count}} person from {{country}}", + "helping": "Helping a new person in {{country}}" +} \ No newline at end of file diff --git a/ui/public/locales/zh/translation.json b/ui/public/locales/zh/translation.json new file mode 100644 index 00000000..bc361032 --- /dev/null +++ b/ui/public/locales/zh/translation.json @@ -0,0 +1,24 @@ +{ + "intro": "Join our network of digital volunteers and help unblock the internet around the world.", + "createdAt": "2024-10-10T11:03:23.027Z", + "updatedAt": "2024-10-10T18:13:49.982Z", + "locale": "en", + "title": "Use your browser to fight global internet censorship", + "status": "Status:", + "keep": "Keep this site open to continue sharing your connection.", + "on": "ON", + "off": "OFF", + "now": "People you're helping right now:", + "total": "Total people helped to date:", + "donate": "Donate to Lantern", + "learn": "Learn More", + "peers": "{{count}} people from {{country}}", + "installChrome": "Install on Chrome", + "installFirefox": "Install on Firefox", + "installCta": "Help even more people by installing the extension", + "waiting": "Waiting for connections", + "installCtaBtnLg": "Get Browsers Unbounded for {{browser}}", + "installCtaBtnSm": "Add to {{browser}}", + "peer": "{{count}} person from {{country}}", + "helping": "Helping a new person in {{country}}" +} \ No newline at end of file diff --git a/ui/src/App.tsx b/ui/src/App.tsx index abf11ac5..ab57ef4f 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useLayoutEffect, useRef, useState} from 'react' +import React, {Suspense, useEffect, useLayoutEffect, useRef, useState} from 'react' import {settingsEmitter} from './index' import Layout from './layout' import Toast from './components/molecules/toast' @@ -15,6 +15,7 @@ import useMessaging from './hooks/useMessaging' import {Targets, Layouts} from './constants' import {AppContextProvider} from './context' import useAutoUpdate, {AUTO_START_STORAGE_FLAG} from './hooks/useAutoUpdate' +import './i18n' interface Props { appId: number @@ -90,16 +91,18 @@ const App = ({appId, embed}: Props) => { { settings.target !== Targets.EXTENSION_POPUP && } { settings.editor && } - - { settings.layout === Layouts.BANNER && ( - - )} - { settings.layout === Layouts.PANEL && ( - - )} - { settings.layout === Layouts.FLOATING && ( - - )} + + + { settings.layout === Layouts.BANNER && ( + + )} + { settings.layout === Layouts.PANEL && ( + + )} + { settings.layout === Layouts.FLOATING && ( + + )} + ); diff --git a/ui/src/components/atoms/extensionButton/index.tsx b/ui/src/components/atoms/extensionButton/index.tsx index d2b7548f..b0b9ad94 100644 --- a/ui/src/components/atoms/extensionButton/index.tsx +++ b/ui/src/components/atoms/extensionButton/index.tsx @@ -5,6 +5,7 @@ import {ChromeColor, FirefoxColor} from '../icons' import {useContext} from 'react' import {AppContext} from '../../../context' import {isFirefox} from '../../../utils/userAgent' +import {useTranslation} from 'react-i18next' const StyledLink = styled.a` display: flex; @@ -27,6 +28,7 @@ interface Props { } const ExtensionButton = ({isSmall}: Props) => { + const {t} = useTranslation() const {width} = useContext(AppContext) const largePadding = width < 900 ? '12px 16px' : '12px 40px' @@ -50,7 +52,7 @@ const ExtensionButton = ({isSmall}: Props) => { lineHeight: '24px', }} > - {isSmall ? `Add to ${isFirefox() ? 'Firefox' : 'Chrome'}` : `Get Browsers Unbounded for ${isFirefox() ? 'Firefox' : 'Chrome'}`} + {isSmall ? `${t('installCtaBtnSm', {browser: isFirefox() ? 'Firefox' : 'Chrome'})}` : `${t('installCtaBtnLg', {browser: isFirefox() ? 'Firefox' : 'Chrome'})}`} ) diff --git a/ui/src/components/molecules/about/index.tsx b/ui/src/components/molecules/about/index.tsx index f3c39b1b..d9c1b205 100644 --- a/ui/src/components/molecules/about/index.tsx +++ b/ui/src/components/molecules/about/index.tsx @@ -2,19 +2,21 @@ import {Text} from './styles' import {useContext, CSSProperties} from 'react' import {AppContext} from '../../../context' import {COLORS, Themes} from '../../../constants' +import {useTranslation} from 'react-i18next' interface Props { style?: CSSProperties } const About = ({style = {}}: Props) => { + const { t } = useTranslation(); const {theme, keepText, infoLink} = useContext(AppContext).settings const color = theme === Themes.DARK ? COLORS.grey2 : COLORS.blue5 return( - {'Join our network of digital volunteers and help unblock the internet around the world.'} + {t('intro')} {/*Lantern.*/} { !!keepText && ' Keep this site open to continue sharing your connection.' } { !!infoLink.length && } diff --git a/ui/src/components/molecules/control/index.tsx b/ui/src/components/molecules/control/index.tsx index 302c6f5b..fd7d2b32 100644 --- a/ui/src/components/molecules/control/index.tsx +++ b/ui/src/components/molecules/control/index.tsx @@ -8,6 +8,7 @@ import {useContext, useState} from 'react' import {AppContext} from '../../../context' import {COLORS, Layouts, Targets} from '../../../constants' import {tutorialOnEmitter} from '../../atoms/tutorial' +import {useTranslation} from 'react-i18next' interface Props { onToggle?: (s: boolean) => void @@ -15,6 +16,7 @@ interface Props { } const Control = ({onToggle, info = false}: Props) => { + const {t} = useTranslation() const ready = useEmitterState(readyEmitter) // true const sharing = useEmitterState(sharingEmitter) const {wasmInterface, settings} = useContext(AppContext) @@ -50,7 +52,7 @@ const Control = ({onToggle, info = false}: Props) => { - Status: {sharing ? 'ON' : 'OFF'} + {t('status')} {sharing ? t('on') : t('off')} { info && } diff --git a/ui/src/components/molecules/extensionCta/index.tsx b/ui/src/components/molecules/extensionCta/index.tsx index 26cddf54..0c9e2d78 100644 --- a/ui/src/components/molecules/extensionCta/index.tsx +++ b/ui/src/components/molecules/extensionCta/index.tsx @@ -3,12 +3,14 @@ import {AppContext} from '../../../context' import {Container} from './styles' import ExtensionButton from '../../atoms/extensionButton' import {Text} from '../../atoms/typography' +import {useTranslation} from 'react-i18next' interface Props { isSmall?: boolean } const ExtensionCta = ({isSmall}: Props) => { + const {t} = useTranslation() const {settings} = useContext(AppContext) const {theme, menu} = settings @@ -26,7 +28,7 @@ const ExtensionCta = ({isSmall}: Props) => { lineHeight: '20px' }} > - Help even more people by installing the extension + {t('installCta')} ) diff --git a/ui/src/components/molecules/globe/index.tsx b/ui/src/components/molecules/globe/index.tsx index 1436af2c..447de9cc 100644 --- a/ui/src/components/molecules/globe/index.tsx +++ b/ui/src/components/molecules/globe/index.tsx @@ -17,6 +17,7 @@ import {useGeo} from '../../../hooks/useGeoFuture' import {Notification} from '../notification' import usePageVisibility from '../../../hooks/usePageVisibility' import * as THREE from 'three' +import { useTranslation } from 'react-i18next'; // import {useEmitterState} from '../../../hooks/useStateEmitter' // import {sharingEmitter} from '../../../utils/wasmInterface' // import {countries} from "../../../utils/countries"; @@ -75,6 +76,7 @@ const materialYellow = new THREE.MeshLambertMaterial({ }); const Globe = ({target}: Props) => { + const {t} = useTranslation() // const sharing = useEmitterState(sharingEmitter) const {width, settings} = useContext(AppContext) const {theme, title, menu} = settings @@ -270,7 +272,13 @@ const Globe = ({target}: Props) => { // ringPropagationSpeed={1} /> { + const country = arc.country.split(',')[0] + const plural = count > 1 + if (plural) return t('peers', { country, count }) + return t('peer', { country, count }) + })} show={!!arc} container={container} /> diff --git a/ui/src/components/molecules/info/index.tsx b/ui/src/components/molecules/info/index.tsx index 7126be6c..c389fcb1 100644 --- a/ui/src/components/molecules/info/index.tsx +++ b/ui/src/components/molecules/info/index.tsx @@ -4,8 +4,10 @@ import {Text} from '../../atoms/typography' import {useContext, useState} from 'react' import { Popover } from 'react-tiny-popover' import {AppContext} from '../../../context' +import {useTranslation} from 'react-i18next' const Info = () => { + const {t} = useTranslation() const {theme, keepText, infoLink} = useContext(AppContext).settings const [active, setActive] = useState(false) return ( @@ -20,9 +22,9 @@ const Info = () => { theme={theme} > - {'Join our network of digital volunteers and help unblock the internet around the world.'} + {t('intro')} {/*Lantern.*/} - { !!keepText && ' Keep this site open to continue sharing your connection.' } + { !!keepText && ` ${t('keep')}` } { !!infoLink.length && } diff --git a/ui/src/components/molecules/menu/index.tsx b/ui/src/components/molecules/menu/index.tsx index b34a4f7b..387024d0 100644 --- a/ui/src/components/molecules/menu/index.tsx +++ b/ui/src/components/molecules/menu/index.tsx @@ -9,17 +9,18 @@ import {isFirefox} from '../../../utils/userAgent' import {useEmitterState} from '../../../hooks/useStateEmitter' import {lifetimeConnectionsEmitter} from '../../../utils/wasmInterface' import {humanizeCount} from '../../../utils/humanize' +import {useTranslation} from 'react-i18next' const menuItems = (connected: number | string) => [ { key: 'firefox', - label: 'Install Firefox Extension', + label: 'installFirefox', href: APP_STORE_LINKS.firefox, icon: }, { key: 'chrome', - label: 'Install Chrome Extension', + label: 'installChrome', href: APP_STORE_LINKS.chrome, icon: }, @@ -31,13 +32,13 @@ const menuItems = (connected: number | string) => [ // }, { key: 'donate', - label: 'Donate to Lantern', + label: 'donate', href: 'https://lantern.io/donate', icon: }, { key: 'more', - label: 'Learn More', + label: 'learn', href: 'https://unbounded.lantern.io', icon: } @@ -47,6 +48,7 @@ interface MenuProps { setExpanded?: Dispatch> | null } const Menu = ({setExpanded} : MenuProps) => { + const {t} = useTranslation() const {settings, width} = useContext(AppContext) const lifetimeConnections = useEmitterState(lifetimeConnectionsEmitter) const {theme, layout, target, collapse} = settings @@ -108,7 +110,7 @@ const Menu = ({setExpanded} : MenuProps) => { style={{color: theme === Themes.LIGHT ? COLORS.blue5 : COLORS.white}} > {item.icon} - {item.label} + {t(item.label)} ) diff --git a/ui/src/components/molecules/stats/index.tsx b/ui/src/components/molecules/stats/index.tsx index 90bc4f30..52b11802 100644 --- a/ui/src/components/molecules/stats/index.tsx +++ b/ui/src/components/molecules/stats/index.tsx @@ -9,15 +9,17 @@ import {connectionsEmitter, lifetimeConnectionsEmitter} from '../../../utils/was import {humanizeCount} from '../../../utils/humanize' import {LifetimeConnectionsWrapper} from './styles' import {Layouts} from '../../../constants' +import {useTranslation} from 'react-i18next' // import TwitterLink from '../../atoms/twitterLink' // import useSample from '../../../hooks/useSample' export const Connections = () => { + const {t} = useTranslation() const connections = useEmitterState(connectionsEmitter) const currentConnections = connections.filter(c => c.state === 1).length return ( <> - People you're helping right now: + {t('now')} @@ -29,6 +31,7 @@ export const Connections = () => { const Stats = () => { + const {t} = useTranslation() const {settings} = useContext(AppContext) const {menu, layout} = settings const connections = useEmitterState(connectionsEmitter) @@ -47,7 +50,7 @@ const Stats = () => { - {'People you\'re helping' + (true ? ' right now:' : ':')} + {(t('now'))} {currentConnections} {/* { { menu ? ( - Total people helped to date: + {t('total')} ) : ( - Total people helped to date: + {t('total')} {/**/} diff --git a/ui/src/components/molecules/title/index.tsx b/ui/src/components/molecules/title/index.tsx index b8172cd9..21d08d5e 100644 --- a/ui/src/components/molecules/title/index.tsx +++ b/ui/src/components/molecules/title/index.tsx @@ -2,19 +2,21 @@ import {Text} from './styles' import {useContext, CSSProperties} from 'react' import {AppContext} from '../../../context' import {COLORS, Themes} from '../../../constants' +import {useTranslation} from 'react-i18next' interface Props { style?: CSSProperties } const Title = ({style = {}}: Props) => { + const { t } = useTranslation(); const {theme} = useContext(AppContext).settings const color = theme === Themes.DARK ? COLORS.grey2 : COLORS.blue5 return( - {'Use your browser to fight global internet censorship'} + {t('title')} ) } diff --git a/ui/src/components/molecules/toast/index.tsx b/ui/src/components/molecules/toast/index.tsx index a1ba8049..d292f01b 100644 --- a/ui/src/components/molecules/toast/index.tsx +++ b/ui/src/components/molecules/toast/index.tsx @@ -6,8 +6,10 @@ import {useEmitterState} from '../../../hooks/useStateEmitter' import {sharingEmitter} from '../../../utils/wasmInterface' import {AppContext} from '../../../context' import {COLORS, Layouts, Targets, Themes} from '../../../constants' +import {useTranslation} from 'react-i18next' const Toast = () => { + const {t} = useTranslation() const {exit, target, toast} = useContext(AppContext).settings const sharing = useEmitterState(sharingEmitter) const {theme, layout} = useContext(AppContext).settings @@ -52,7 +54,7 @@ const Toast = () => { - Keep this site open to continue sharing your connection + {t('keep')} ) diff --git a/ui/src/hooks/useGeoFuture.ts b/ui/src/hooks/useGeoFuture.ts index 8c6088f3..0dd3f8a7 100644 --- a/ui/src/hooks/useGeoFuture.ts +++ b/ui/src/hooks/useGeoFuture.ts @@ -5,6 +5,7 @@ import {countries} from '../utils/countries' import {pushNotification} from '../components/molecules/notification' import {AppContext} from '../context' import {Targets} from '../constants' +import {useTranslation} from 'react-i18next' type ISO = keyof typeof countries @@ -125,6 +126,7 @@ const incrementArcs = (arcs: Arch[], geos: GeoLookup[]) => { } export const useGeo = () => { + const {t} = useTranslation() const [arcs, setArcs] = useState([]) const [points, setPoints] = useState([]) const country = useRef() @@ -178,7 +180,8 @@ export const useGeo = () => { if (target === Targets.EXTENSION_POPUP && startTs.current + 1000 > performance.now()) return // don't show notifications on initial load because of initial sync w/ bg script pushNotification({ id: geo.workerIdx, - text: `Helping a new person in ${country.split(',')[0]}`, + // text: `Helping a new person in ${country.split(',')[0]}`, + text: `${t('helping', {country})}`, autoHide: true, heart: true }) @@ -201,7 +204,7 @@ export const useGeo = () => { useEffect(() => { if (sharing && !active) pushNotification({ id: -1, - text: 'Waiting for connections', + text: t('waiting'), ellipse: true }) }, [sharing, active]) diff --git a/ui/src/i18n.js b/ui/src/i18n.js new file mode 100644 index 00000000..ccb0affb --- /dev/null +++ b/ui/src/i18n.js @@ -0,0 +1,32 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; + +import Backend from 'i18next-http-backend'; +import LanguageDetector from 'i18next-browser-languagedetector'; +// don't want to use this? +// have a look at the Quick start guide +// for passing in lng and translations on init + +i18n + // load translation using http -> see /public/locales (i.e. https://github.com/i18next/react-i18next/tree/master/example/react/public/locales) + // learn more: https://github.com/i18next/i18next-http-backend + // want your translations to be loaded from a professional CDN? => https://github.com/locize/react-tutorial#step-2---use-the-locize-cdn + .use(Backend) + // detect user language + // learn more: https://github.com/i18next/i18next-browser-languageDetector + .use(LanguageDetector) + // pass the i18n instance to react-i18next. + .use(initReactI18next) + // init i18next + // for all options read: https://www.i18next.com/overview/configuration-options + .init({ + fallbackLng: 'en', + debug: true, + + interpolation: { + escapeValue: false, // not needed for react as it escapes by default + } + }); + + +export default i18n; \ No newline at end of file diff --git a/ui/translate.js b/ui/translate.js new file mode 100644 index 00000000..d82a95a4 --- /dev/null +++ b/ui/translate.js @@ -0,0 +1,85 @@ +require('dotenv').config({ path: '.env.production' }); +const axios = require('axios'); +const fs = require('fs'); + +const headers = { + 'Authorization': `Bearer ${process.env.STRAPI_API_TOKEN}`, +} +const baseUrl = process.env.STRAPI_API_URL + +const fallbackLocale = 'en'; + +const fallBackGet = async (route, params) => { + return await axios.get(baseUrl + '/' + route, { + params: {...params, locale: fallbackLocale}, + headers + }) +} + +// { locale?: string } +const get = async (route, params) => { + const {data} = await axios.get(baseUrl + '/' + route, { + params, + headers + }) + .catch(async (err) => { + if (params.locale && params.locale !== fallbackLocale) return await fallBackGet(route) + else { + console.error('Error fetching data from Strapi:', err.message) + return err + } + }) + return data +}; + +const ACCEPTED_LOCALES = [ + 'en', + 'en-US', // browser default + 'ar', + 'zh', // zh chinese fallback + 'zh-Hans', // default zh + // 'zh-Hans-HK', + 'zh-Hant-TW', + 'ru', + 'fa', + 'es-419' +]; + +const getLocales = async () => { + const locales = await get('i18n/locales', {}); + // create a zh locale for chinese fallback + const zhHans = locales.find(locale => locale.code === 'zh-Hans'); + const lastId = locales[locales.length - 1].id; + if (zhHans) locales.push({ + ...zhHans, + code: 'zh', + id: lastId + 1 + }); + // create a redundant en-US locale for english browser behavior + const en = locales.find(locale => locale.code === 'en'); + if (en) locales.push({ + ...en, + code: 'en-US', + id: lastId + 2 + }); + return locales.filter(locale => ACCEPTED_LOCALES.includes(locale.code)); +} + +const runAsync = async () => { + const locales = await getLocales(); + + for (const locale of locales) { + console.log(`Translating items for ${locale.code}`); + const translations = await get('unbounded-widget', { locale: locale.code }); + const {data} = translations; + const {attributes} = data; + // create an i18n file for each locale + const localeData = attributes; + if (!fs.existsSync(`public/locales/${locale.code}`)) { + fs.mkdirSync(`public/locales/${locale.code}`, { recursive: true }); + } + fs.writeFileSync(`public/locales/${locale.code}/translation.json`, JSON.stringify(localeData, null, 2)); + } +} + +runAsync(); \ No newline at end of file diff --git a/ui/yarn.lock b/ui/yarn.lock index e90c42df..55873362 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -1129,6 +1129,13 @@ dependencies: regenerator-runtime "^0.13.11" +"@babel/runtime@^7.23.2", "@babel/runtime@^7.25.0": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.7.tgz#7ffb53c37a8f247c8c4d335e89cdf16a2e0d0fb6" + integrity sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.22.5", "@babel/template@^7.3.3": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" @@ -3006,6 +3013,15 @@ axe-core@^4.6.2: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.2.tgz#040a7342b20765cb18bb50b628394c21bccc17a0" integrity sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g== +axios@^1.7.7: + version "1.7.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" + integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axobject-query@^3.1.1: version "3.2.1" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a" @@ -3672,6 +3688,13 @@ cosmiconfig@^7.0.0: path-type "^4.0.0" yaml "^1.10.0" +cross-fetch@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.0.0.tgz#f037aef1580bb3a1a35164ea2a848ba81b445983" + integrity sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g== + dependencies: + node-fetch "^2.6.12" + cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -5097,6 +5120,11 @@ follow-redirects@^1.0.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== +follow-redirects@^1.15.6: + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -5132,6 +5160,15 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.1.tgz#ba1076daaaa5bfd7e99c1a6cb02aa0a5cff90d48" + integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -5561,6 +5598,13 @@ html-minifier-terser@^6.0.2: relateurl "^0.2.7" terser "^5.10.0" +html-parse-stringify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2" + integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg== + dependencies: + void-elements "3.1.0" + html-webpack-plugin@^5.5.0: version "5.5.3" resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.5.3.tgz#72270f4a78e222b5825b296e5e3e1328ad525a3e" @@ -5655,6 +5699,27 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +i18next-browser-languagedetector@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz#b6fdd9b43af67c47f2c26c9ba27710a1eaf31e2f" + integrity sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw== + dependencies: + "@babel/runtime" "^7.23.2" + +i18next-http-backend@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/i18next-http-backend/-/i18next-http-backend-2.6.2.tgz#b25516446ae6f251ce8231e70e6ffbca833d46a5" + integrity sha512-Hp/kd8/VuoxIHmxsknJXjkTYYHzivAyAF15pzliKzk2TiXC25rZCEerb1pUFoxz4IVrG3fCvQSY51/Lu4ECV4A== + dependencies: + cross-fetch "4.0.0" + +i18next@^23.15.2: + version "23.15.2" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.15.2.tgz#8a54f877ccbbc46696eacb5bd5b31d84f9ade7cb" + integrity sha512-zcPSWzCvw6uKnuYHIqs4W7hTuB9e3AFcSdZgvCWoPXIZsBjBd4djN2/2uOHIB+1DFFkQnMBXvhNg7J3WyCuywQ== + dependencies: + "@babel/runtime" "^7.23.2" + iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -7162,6 +7227,13 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" +node-fetch@^2.6.12: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + node-forge@^1: version "1.3.1" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" @@ -8231,6 +8303,11 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + psl@^1.1.33: version "1.9.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" @@ -8356,6 +8433,14 @@ react-globe.gl@^2.22.4: prop-types "15" react-kapsule "2" +react-i18next@^15.0.2: + version "15.0.2" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.0.2.tgz#8b7f3c0e66cb4f99f95e2c507353398c41a68cc2" + integrity sha512-z0W3/RES9Idv3MmJUcf0mDNeeMOUXe+xoL0kPfQPbDoZHmni/XsIoq5zgT2MCFUiau283GuBUK578uD/mkAbLQ== + dependencies: + "@babel/runtime" "^7.25.0" + html-parse-stringify "^3.0.1" + react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -8519,6 +8604,11 @@ regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.9: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + regenerator-transform@^0.15.1: version "0.15.1" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.1.tgz#f6c4e99fc1b4591f780db2586328e4d9a9d8dc56" @@ -9627,6 +9717,11 @@ tr46@^2.1.0: dependencies: punycode "^2.1.1" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + trim-repeated@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-repeated/-/trim-repeated-1.0.0.tgz#e3646a2ea4e891312bf7eace6cfb05380bc01c21" @@ -9918,6 +10013,11 @@ vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== +void-elements@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" + integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== + w3c-hr-time@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" @@ -9959,6 +10059,11 @@ web-vitals@^2.1.4: resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-2.1.4.tgz#76563175a475a5e835264d373704f9dde718290c" integrity sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg== +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + webidl-conversions@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" @@ -10131,6 +10236,14 @@ whatwg-mimetype@^2.3.0: resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + whatwg-url@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"