Browse Source

Merge pull request #9 from naresh97/feature/i18n

add internationalization

currently only malay language supported. skeleton for tamil and chinese is setup.
development
Nareshkumar Rao 3 years ago
committed by GitHub
parent
commit
8a52117169
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      package.json
  2. 2
      src/app/store.js
  3. 46
      src/components/LanguageSwitcher.js
  4. 21
      src/features/auth/langSlice.js
  5. 34
      src/i18n.js
  6. 1
      src/index.js
  7. 49
      src/locales/en/common.json
  8. 49
      src/locales/ms/common.json
  9. 49
      src/locales/ta/common.json
  10. 49
      src/locales/zh/common.json
  11. 48
      src/screens/HomeScreen.js
  12. 12
      src/screens/LockoutScreen.js
  13. 32
      src/screens/LoginScreen.js
  14. 18
      src/screens/ScannerScreen.js
  15. 9
      src/screens/SuccessScreen.js
  16. 12
      src/screens/VerifyScreen.js
  17. 37
      yarn.lock

4
package.json

@ -11,12 +11,14 @@
"@testing-library/react": "^10.2.1", "@testing-library/react": "^10.2.1",
"@testing-library/user-event": "^12.0.2", "@testing-library/user-event": "^12.0.2",
"axios": "^0.21.1", "axios": "^0.21.1",
"country-flag-icons": "^1.4.0",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"framer-motion": "^4", "framer-motion": "^4",
"i18next": "^20.3.5",
"js-cookie": "^2.2.1", "js-cookie": "^2.2.1",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-icons": "^3.0.0",
"react-i18next": "^11.11.4",
"react-qr-reader": "^2.2.1", "react-qr-reader": "^2.2.1",
"react-redux": "^7.2.4", "react-redux": "^7.2.4",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",

2
src/app/store.js

@ -1,10 +1,12 @@
import { configureStore } from '@reduxjs/toolkit'; import { configureStore } from '@reduxjs/toolkit';
import authSlice from '../features/auth/authSlice'; import authSlice from '../features/auth/authSlice';
import covidSlice from '../features/auth/covidSlice'; import covidSlice from '../features/auth/covidSlice';
import langSlice from '../features/auth/langSlice';
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
auth: authSlice, auth: authSlice,
covid: covidSlice, covid: covidSlice,
lang: langSlice,
}, },
}); });

46
src/components/LanguageSwitcher.js

@ -0,0 +1,46 @@
import { Select } from '@chakra-ui/react';
import Flags from 'country-flag-icons/react/3x2';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { setLanguage } from '../features/auth/langSlice';
function LanguageSwitcher() {
const [, i18n] = useTranslation();
const dispatch = useDispatch();
const currentLangIcon = (() => {
switch (i18n.language) {
case 'en':
return <Flags.GB />;
case 'ms':
return <Flags.MY />;
case 'zh':
return <Flags.CN />;
case 'ta':
return <Flags.IN />;
default:
return <Flags.GB />;
}
})();
const handleSelectLanguage = e => {
const languageKey = e.target.value;
i18n.changeLanguage(languageKey);
dispatch(setLanguage(languageKey));
};
return (
<Select
value={i18n.language}
icon={currentLangIcon}
variant="filled"
onChange={handleSelectLanguage}
>
<option value="en">English</option>
<option value="ms">Bahasa Melayu</option>
{/*<option value="zh"></option>
<option value="ta">தமிழ்</option>*/}
</Select>
);
}
export default LanguageSwitcher;

21
src/features/auth/langSlice.js

@ -0,0 +1,21 @@
import { createSlice } from '@reduxjs/toolkit';
import Cookies from 'js-cookie';
const initialState = {
lang: Cookies.get('lang'),
};
export const langSlice = createSlice({
name: 'lang',
initialState,
reducers: {
setLanguage: (state, action) => {
state.lang = action.payload;
Cookies.set('lang', action.payload);
},
},
});
export const { setLanguage } = langSlice.actions;
export default langSlice.reducer;

34
src/i18n.js

@ -0,0 +1,34 @@
import i18n from 'i18next';
import Cookies from 'js-cookie';
import { initReactI18next } from 'react-i18next';
import enTranslation from './locales/en/common.json';
import msTranslation from './locales/ms/common.json';
import taTranslation from './locales/ta/common.json';
import zhTranslation from './locales/zh/common.json';
const resources = {
en: {
common: enTranslation,
},
ms: {
common: msTranslation,
},
zh: {
common: zhTranslation,
},
ta: {
common: taTranslation,
},
};
i18n.use(initReactI18next).init({
resources,
lng: Cookies.get('lang') ? Cookies.get('lang') : 'en',
defaultNS: 'common',
interpolation: {
escapeValue: false,
},
debug: true,
});
export default i18n;

1
src/index.js

@ -6,6 +6,7 @@ import App from './App';
import { store } from './app/store'; import { store } from './app/store';
import reportWebVitals from './reportWebVitals'; import reportWebVitals from './reportWebVitals';
import * as serviceWorker from './serviceWorker'; import * as serviceWorker from './serviceWorker';
import './i18n';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
require('dotenv').config(); require('dotenv').config();

49
src/locales/en/common.json

@ -0,0 +1,49 @@
{
"appTitle": "OurSejahtera Contact Tracing",
"homeExplanation": "This is your QR code. Show this to others to allow them to confirm a contact, or allow them to create an account!",
"scanButtonLabel": "Scan a QR Code",
"donateButtonLabel": "Donate!",
"donateButtonParagraph": "Servers require money to run, and apps require labor to develop and maintain. You can show your support by donating what you can. Every cent counts, buy me my next coffee, or help pay for a month of server usage!",
"logOutButtonLabel": "Log Out!",
"covidPositiveReportButton": "Report Positive COVID19",
"covidPositiveAlertHeader": "Confirm Tested COVID19 Positive",
"covidPositiveAlertBody": "Please confirm that you have been tested POSITIVE with COVID19. Upon confirmation, this app will inform the people you have come in contact with in the last 7 days.",
"confirm": "Confirm",
"cancel": "Cancel",
"errorToastTitle": "Error!",
"defaultErrorToastDescription": "An error has occured.",
"confirmedToastTitle": "Confirmed!",
"confirmingToastTitle": "Confirming",
"confirmingToastDescription": "Hold on while we confirm with our servers.",
"notLoggedInToastDescription": "You are not logged in!",
"failedLoginToastTitle": "Login Failed",
"failedLoginToastDescription": "The wrong credentials were used",
"loggingInToastTitle": "Logging you in",
"loggingInToastDescription": "Hold on, we're logging you in.",
"checkingLockoutToastTitle": "Checking your lockout status...",
"badVerificationToastTitle": "Bad Verification",
"badQRCodeToastTitle": "Bad QR Code",
"contactLoggedToastTitle": "Contact Successfully Logged",
"checkingQRToastTitle": "Hold on, we're checking this QR code.",
"login": "Login",
"loginPrivacyNotice": "<0><0>Privacy notes:</0> <br/>Telegram Login allows us to verify your identity, without collecting any of your data. Telegram does NOT give us your phone number. The only piece of information stored on our server is your Telegram ID, this is an internal ID Number Telegram uses that is SEPARATE from your Telegram Username.<br/><br/>All the code for this project is <8>Open Source</8>, that means anyone, including you can audit and verify that your information is being handled securely.</0>",
"lockout": "Lockout",
"lockoutExplanation": "<0>You have reported that you have been tested <1>POSITIVE</1> with COVID19. This lockout is to remind you to quarantine yourself according to local COVID19 health policies. This lockout will automatically be lifted after 14 days.<br/><br/><5>Please avoid contact with other people for the duration of this lockout!</5></0>",
"showQRCode": "Show my QR Code",
"contactSavedParagraph": "We have succesfully saved your contact! Stay safe out there, and let others scan your QR code too!",
"returnHomeButtonLabel": "Return home",
"verifyingParagraph": "We are currently verifying you. Please wait.",
"verifyingErrorParagraph": "An error has occured verifying you. Please try scanning the QR code again?"
}

49
src/locales/ms/common.json

@ -0,0 +1,49 @@
{
"appTitle": "Pengesanan Kontak OurSejahtera",
"homeExplanation": "Ini ialah kod QR anda. Tunjukkan kepada pengguna lain untuk mengesahkan kenalan.",
"scanButtonLabel": "Imbas kod QR",
"donateButtonLabel": "Derma!",
"donateButtonParagraph": "Memelihara server memerlukan wang dan membuat serta memantau aplikasi memerlukan tenaga kerja. Anda boleh menghulurkan bantuan dengan menderma mengikut kemampuan. Setiap sen ada nilainya.",
"logOutButtonLabel": "Daftar Keluar!",
"covidPositiveReportButton": "Laporkan Positif COVID19",
"covidPositiveAlertHeader": "Sahkan Diuji Positif COVID19",
"covidPositiveAlertBody": "Sila sahkan bahawa anda telah diuji positif COVID19. Atas pengesahan, aplikasi ini akan memaklumkan para individu yang telah bersemuka dengan anda dalam 7 hari yang lepas.",
"confirm": "Sahkan",
"cancel": "Batalkan",
"errorToastTitle": "Ralat!",
"defaultErrorToastDescription": "Ralat Berlaku.",
"confirmedToastTitle": "Disahkan!",
"confirmingToastTitle": "Mengesahkan",
"confirmingToastDescription": "Sila tunggu sebentar sementara kami sedang mengesahkan dengan server kami.",
"notLoggedInToastDescription": "Anda tidak didaftar masuk!",
"failedLoginToastTitle": "Gagal Daftar Masuk",
"failedLoginToastDescription": "Kelayakan yang salah telah digunakan",
"loggingInToastTitle": "Mendaftar masuk",
"loggingInToastDescription": "Sila tunggu sebentar sementara anda didaftar masuk.",
"checkingLockoutToastTitle": "Memeriksa status lockout anda...",
"badVerificationToastTitle": "Pengesahan Tidak Berfungsi",
"badQRCodeToastTitle": "Kod QR Tidak Berfungsi",
"contactLoggedToastTitle": "Kontak Berjaya Didaftar",
"checkingQRToastTitle": "Sila tunggu sebentar. Kami sedang memeriksa kod QR ini.",
"login": "Daftar Masuk",
"loginPrivacyNotice": "<0><0>Notis Privasi:</0> <br/>Pendaftaran masuk melalui Telegram membolehkan pengesahan identiti anda tanpa pengumpulan data peribadi anda. Telegram TIDAK mendedahkan nombor telefon anda. Hanya ID Telegram anda akan disimpan dalam server kami. Ini ialah Nombor ID Telegram dalaman yang digunakan oleh Telegram dan BUKANNYA nama pengguna Telegram anda.<br/><br/>Segala kod untuk projek ini ialah <8>Sumber Terbuka</8>, bermaksud sesiapa sahaja, termasuk diri anda boleh mengaudit dan mengesahkan bahawa segala maklumat dikendalikan dengan selamat.</0>",
"lockout": "Lockout",
"lockoutExplanation": "<0>Anda telah laporkan bahawa anda diuji <1>POSITIF</1> COVID19. Lockout ini adalah untuk mengingatkan anda supaya mengkuarantinkan diri mengikut polisi kesihatan COVID19 tempatan. Lockout ini akan dibatalkan selepas 14 hari.<br/><br/><5>Anda diminta untuk elak daripada berinteraksi dengan orang lain sewaktu lockout ini.</5></0>",
"showQRCode": "Tunjuk kod QR saya",
"contactSavedParagraph": "Kontak anda berjaya disimpan! Jaga kesejahteraan diri dan berikan orang lain imbas kod QR anda juga!",
"returnHomeButtonLabel": "Kembali",
"verifyingParagraph": "Kami sedang mengesahkan anda. Sila tunggu sebentar.",
"verifyingErrorParagraph": "Ralat berlaku sewaktu pengesahan. Sila imbas kod QR sekali lagi."
}

49
src/locales/ta/common.json

@ -0,0 +1,49 @@
{
"appTitle": "OurSejahtera Contact Tracing",
"homeExplanation": "This is your QR code. Show this to others to allow them to confirm a contact, or allow them to create an account!",
"scanButtonLabel": "Scan a QR Code",
"donateButtonLabel": "Donate!",
"donateButtonParagraph": "Servers require money to run, and apps require labor to develop and maintain. You can show your support by donating what you can. Every cent counts, buy me my next coffee, or help pay for a month of server usage!",
"logOutButtonLabel": "Log Out!",
"covidPositiveReportButton": "Report Positive COVID19",
"covidPositiveAlertHeader": "Confirm Tested COVID19 Positive",
"covidPositiveAlertBody": "Please confirm that you have been tested POSITIVE with COVID19. Upon confirmation, this app will inform the people you have come in contact with in the last 7 days.",
"confirm": "Confirm",
"cancel": "Cancel",
"errorToastTitle": "Error!",
"defaultErrorToastDescription": "An error has occured.",
"confirmedToastTitle": "Confirmed!",
"confirmingToastTitle": "Confirming",
"confirmingToastDescription": "Hold on while we confirm with our servers.",
"notLoggedInToastDescription": "You are not logged in!",
"failedLoginToastTitle": "Login Failed",
"failedLoginToastDescription": "The wrong credentials were used",
"loggingInToastTitle": "Logging you in",
"loggingInToastDescription": "Hold on, we're logging you in.",
"checkingLockoutToastTitle": "Checking your lockout status...",
"badVerificationToastTitle": "Bad Verification",
"badQRCodeToastTitle": "Bad QR Code",
"contactLoggedToastTitle": "Contact Successfully Logged",
"checkingQRToastTitle": "Hold on, we're checking this QR code.",
"login": "Login",
"loginPrivacyNotice": "<0><0>Privacy notes:</0> <br/>Telegram Login allows us to verify your identity, without collecting any of your data. Telegram does NOT give us your phone number. The only piece of information stored on our server is your Telegram ID, this is an internal ID Number Telegram uses that is SEPARATE from your Telegram Username.<br/><br/>All the code for this project is <8>Open Source</8>, that means anyone, including you can audit and verify that your information is being handled securely.</0>",
"lockout": "Lockout",
"lockoutExplanation": "<0>You have reported that you have been tested <1>POSITIVE</1> with COVID19. This lockout is to remind you to quarantine yourself according to local COVID19 health policies. This lockout will automatically be lifted after 14 days.<br/><br/><5>Please avoid contact with other people for the duration of this lockout!</5></0>",
"showQRCode": "Show my QR Code",
"contactSavedParagraph": "We have succesfully saved your contact! Stay safe out there, and let others scan your QR code too!",
"returnHomeButtonLabel": "Return home",
"verifyingParagraph": "We are currently verifying you. Please wait.",
"verifyingErrorParagraph": "An error has occured verifying you. Please try scanning the QR code again?"
}

49
src/locales/zh/common.json

@ -0,0 +1,49 @@
{
"appTitle": "OurSejahtera Contact Tracing",
"homeExplanation": "This is your QR code. Show this to others to allow them to confirm a contact, or allow them to create an account!",
"scanButtonLabel": "Scan a QR Code",
"donateButtonLabel": "Donate!",
"donateButtonParagraph": "Servers require money to run, and apps require labor to develop and maintain. You can show your support by donating what you can. Every cent counts, buy me my next coffee, or help pay for a month of server usage!",
"logOutButtonLabel": "Log Out!",
"covidPositiveReportButton": "Report Positive COVID19",
"covidPositiveAlertHeader": "Confirm Tested COVID19 Positive",
"covidPositiveAlertBody": "Please confirm that you have been tested POSITIVE with COVID19. Upon confirmation, this app will inform the people you have come in contact with in the last 7 days.",
"confirm": "Confirm",
"cancel": "Cancel",
"errorToastTitle": "Error!",
"defaultErrorToastDescription": "An error has occured.",
"confirmedToastTitle": "Confirmed!",
"confirmingToastTitle": "Confirming",
"confirmingToastDescription": "Hold on while we confirm with our servers.",
"notLoggedInToastDescription": "You are not logged in!",
"failedLoginToastTitle": "Login Failed",
"failedLoginToastDescription": "The wrong credentials were used",
"loggingInToastTitle": "Logging you in",
"loggingInToastDescription": "Hold on, we're logging you in.",
"checkingLockoutToastTitle": "Checking your lockout status...",
"badVerificationToastTitle": "Bad Verification",
"badQRCodeToastTitle": "Bad QR Code",
"contactLoggedToastTitle": "Contact Successfully Logged",
"checkingQRToastTitle": "Hold on, we're checking this QR code.",
"login": "Login",
"loginPrivacyNotice": "<0><0>Privacy notes:</0> <br/>Telegram Login allows us to verify your identity, without collecting any of your data. Telegram does NOT give us your phone number. The only piece of information stored on our server is your Telegram ID, this is an internal ID Number Telegram uses that is SEPARATE from your Telegram Username.<br/><br/>All the code for this project is <8>Open Source</8>, that means anyone, including you can audit and verify that your information is being handled securely.</0>",
"lockout": "Lockout",
"lockoutExplanation": "<0>You have reported that you have been tested <1>POSITIVE</1> with COVID19. This lockout is to remind you to quarantine yourself according to local COVID19 health policies. This lockout will automatically be lifted after 14 days.<br/><br/><5>Please avoid contact with other people for the duration of this lockout!</5></0>",
"showQRCode": "Show my QR Code",
"contactSavedParagraph": "We have succesfully saved your contact! Stay safe out there, and let others scan your QR code too!",
"returnHomeButtonLabel": "Return home",
"verifyingParagraph": "We are currently verifying you. Please wait.",
"verifyingErrorParagraph": "An error has occured verifying you. Please try scanning the QR code again?"
}

48
src/screens/HomeScreen.js

@ -16,8 +16,10 @@ import {
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import axios from 'axios'; import axios from 'axios';
import { Fragment, React, useEffect, useRef, useState } from 'react'; import { Fragment, React, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { Redirect, useHistory } from 'react-router-dom'; import { Redirect, useHistory } from 'react-router-dom';
import LanguageSwitcher from '../components/LanguageSwitcher';
import { authLogout } from '../features/auth/authSlice'; import { authLogout } from '../features/auth/authSlice';
import { setCovidPositive } from '../features/auth/covidSlice'; import { setCovidPositive } from '../features/auth/covidSlice';
@ -54,13 +56,14 @@ function ConfirmCOVIDPositiveAlertDialog() {
const toast = useToast(); const toast = useToast();
const history = useHistory(); const history = useHistory();
const dispatch = useDispatch(); const dispatch = useDispatch();
const [t] = useTranslation();
const onClose = () => { const onClose = () => {
setOpen(false); setOpen(false);
}; };
const showErrorToast = (errorMessage = 'An error has occured.') => {
const showErrorToast = (errorMessage = t('defaultErrorToastDescription')) => {
toast.closeAll(); toast.closeAll();
toast({ toast({
title: 'Error!',
title: t('errorToastTitle'),
description: errorMessage, description: errorMessage,
status: 'error', status: 'error',
duration: 5000, duration: 5000,
@ -68,8 +71,8 @@ function ConfirmCOVIDPositiveAlertDialog() {
}; };
const onConfirm = () => { const onConfirm = () => {
toast({ toast({
title: 'Confirming',
description: 'Hold on while we confirm with our servers.',
title: t('confirmingToastTitle'),
description: t('confirmingToastDescription'),
status: 'info', status: 'info',
duration: 10000, duration: 10000,
}); });
@ -86,7 +89,7 @@ function ConfirmCOVIDPositiveAlertDialog() {
dispatch(setCovidPositive()); dispatch(setCovidPositive());
toast.closeAll(); toast.closeAll();
toast({ toast({
title: 'Confirmed!',
title: t('confirmedToastTitle'),
status: 'info', status: 'info',
duration: 2000, duration: 2000,
}); });
@ -98,7 +101,7 @@ function ConfirmCOVIDPositiveAlertDialog() {
console.log(err); console.log(err);
try { try {
if (err.response.status === 401) { if (err.response.status === 401) {
showErrorToast('You are not logged in!');
showErrorToast(t('notLoggedInToastDescription'));
history.push('/login'); history.push('/login');
} else { } else {
showErrorToast(); showErrorToast();
@ -120,7 +123,7 @@ function ConfirmCOVIDPositiveAlertDialog() {
setOpen(true); setOpen(true);
}} }}
> >
Report Positive COVID19
{t('covidPositiveReportButton')}
</Button> </Button>
<AlertDialog <AlertDialog
isOpen={isOpen} isOpen={isOpen}
@ -130,19 +133,15 @@ function ConfirmCOVIDPositiveAlertDialog() {
<AlertDialogOverlay> <AlertDialogOverlay>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
Confirm Tested COVID19 Positive
{t('covidPositiveAlertHeader')}
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogBody>
Please confirm that you have been tested POSITIVE with COVID19.
Upon confirmation, this app will inform the people you have come
in contact with in the last 7 days.
</AlertDialogBody>
<AlertDialogBody>{t('covidPositiveAlertBody')}</AlertDialogBody>
<AlertDialogFooter> <AlertDialogFooter>
<Button ref={cancelRef} onClick={onClose}> <Button ref={cancelRef} onClick={onClose}>
Cancel
{t('cancel')}
</Button> </Button>
<Button colorScheme="red" onClick={onConfirm} ml={3}> <Button colorScheme="red" onClick={onConfirm} ml={3}>
Confirm
{t('confirm')}
</Button> </Button>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
@ -155,6 +154,7 @@ function ConfirmCOVIDPositiveAlertDialog() {
function Home() { function Home() {
const history = useHistory(); const history = useHistory();
const dispatch = useDispatch(); const dispatch = useDispatch();
const [t] = useTranslation();
const handleLogout = () => { const handleLogout = () => {
dispatch(authLogout()); dispatch(authLogout());
@ -198,8 +198,7 @@ function Home() {
> >
<QRCode /> <QRCode />
<Text mb={6} align="center" fontSize="lg"> <Text mb={6} align="center" fontSize="lg">
This is your QR code. Show this to others to allow them to confirm a
contact, or allow them to create an account!
{t('homeExplanation')}
</Text> </Text>
<Divider mb={6} /> <Divider mb={6} />
<Button <Button
@ -208,30 +207,27 @@ function Home() {
history.push('/scanner'); history.push('/scanner');
}} }}
> >
Scan a QR Code
{t('scanButtonLabel')}
</Button> </Button>
{process.env.REACT_APP_DONATE_LINK && ( {process.env.REACT_APP_DONATE_LINK && (
<Fragment> <Fragment>
<Divider mb={6} /> <Divider mb={6} />
<Link href={process.env.REACT_APP_DONATE_LINK}> <Link href={process.env.REACT_APP_DONATE_LINK}>
<Button style={{ width: '100% ' }} mb={6} colorScheme="blue"> <Button style={{ width: '100% ' }} mb={6} colorScheme="blue">
Donate!
{t('donateButtonlabel')}
</Button> </Button>
</Link> </Link>
<Text mb={6}>
Servers require money to run, and apps require labor to develop
and maintain. You can show your support by donating what you can.
Every cent counts, buy me my next coffee, or help pay for a month
of server usage!
</Text>
<Text mb={6}>{t('donateButtonParagraph')}</Text>
</Fragment> </Fragment>
)} )}
<Divider mb={6} /> <Divider mb={6} />
<ConfirmCOVIDPositiveAlertDialog /> <ConfirmCOVIDPositiveAlertDialog />
<Divider mb={6} /> <Divider mb={6} />
<Button colorScheme="blackAlpha" mb={6} onClick={handleLogout}> <Button colorScheme="blackAlpha" mb={6} onClick={handleLogout}>
Log Out!
{t('logOutButtonLabel')}
</Button> </Button>
<Divider mb={6} />
<LanguageSwitcher />
</Flex> </Flex>
</Flex> </Flex>
); );

12
src/screens/LockoutScreen.js

@ -1,6 +1,7 @@
import { Flex, Heading, Text, useToast } from '@chakra-ui/react'; import { Flex, Heading, Text, useToast } from '@chakra-ui/react';
import axios from 'axios'; import axios from 'axios';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { Redirect, useHistory } from 'react-router-dom'; import { Redirect, useHistory } from 'react-router-dom';
import { authLogout } from '../features/auth/authSlice'; import { authLogout } from '../features/auth/authSlice';
@ -15,10 +16,11 @@ function Lockout() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const history = useHistory(); const history = useHistory();
const toast = useToast(); const toast = useToast();
const [t] = useTranslation();
useEffect(() => { useEffect(() => {
toast({ toast({
title: 'Checking your lockout status...',
title: t('checkingLockoutToastTitle'),
status: 'info', status: 'info',
duration: 10000, duration: 10000,
}); });
@ -44,14 +46,14 @@ function Lockout() {
history.push('/login'); history.push('/login');
} else { } else {
toast({ toast({
title: 'Server Error Occurred',
title: t('defaultErrorToastDescription'),
status: 'error', status: 'error',
duration: 10000, duration: 10000,
}); });
} }
} catch (e) {} } catch (e) {}
}); });
}, [dispatch, history, toast]);
}, [dispatch, history, toast, t]);
if (!isAuthenticated) return <Redirect to="/login" />; if (!isAuthenticated) return <Redirect to="/login" />;
if (!isCovidPositive) return <Redirect to="/home" />; if (!isCovidPositive) return <Redirect to="/home" />;
@ -72,7 +74,8 @@ function Lockout() {
rounded={6} rounded={6}
id="contentFlex" id="contentFlex"
> >
<Heading>Lockout</Heading>
<Heading>{t('Lockout')}</Heading>
<Trans i18nKey="lockoutExplanation">
<Text> <Text>
You have reported that you have been tested <b>POSITIVE</b> with You have reported that you have been tested <b>POSITIVE</b> with
COVID19. This lockout is to remind you to quarantine yourself COVID19. This lockout is to remind you to quarantine yourself
@ -85,6 +88,7 @@ function Lockout() {
lockout! lockout!
</b> </b>
</Text> </Text>
</Trans>
</Flex> </Flex>
</Flex> </Flex>
); );

32
src/screens/LoginScreen.js

@ -1,23 +1,27 @@
import { Divider, Flex, Heading, Link, Text, useToast } from '@chakra-ui/react'; import { Divider, Flex, Heading, Link, Text, useToast } from '@chakra-ui/react';
import axios from 'axios'; import axios from 'axios';
import { React } from 'react'; import { React } from 'react';
import { useTranslation } from 'react-i18next';
import { Trans } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { Redirect, useHistory } from 'react-router-dom'; import { Redirect, useHistory } from 'react-router-dom';
import TelegramLoginButton from 'react-telegram-login'; import TelegramLoginButton from 'react-telegram-login';
import LanguageSwitcher from '../components/LanguageSwitcher';
import { authLogin, authLogout } from '../features/auth/authSlice'; import { authLogin, authLogout } from '../features/auth/authSlice';
function Login() { function Login() {
const toast = useToast(); const toast = useToast();
const history = useHistory(); const history = useHistory();
const dispatch = useDispatch(); const dispatch = useDispatch();
const [t] = useTranslation();
const isAuthenticated = useSelector(state => state.auth.isAuthenticated); const isAuthenticated = useSelector(state => state.auth.isAuthenticated);
if (isAuthenticated) return <Redirect to="/home" />; if (isAuthenticated) return <Redirect to="/home" />;
const handleTelegramResponse = response => { const handleTelegramResponse = response => {
toast({ toast({
title: 'Logging you in',
description: "Hold on, we're logging you in.",
title: t('loggingInToastTitle'),
description: t('loggingInToastDescription'),
status: 'info', status: 'info',
duration: 10000, duration: 10000,
isClosable: false, isClosable: false,
@ -45,7 +49,7 @@ function Login() {
toast.closeAll(); toast.closeAll();
dispatch(authLogout()); dispatch(authLogout());
toast({ toast({
title: 'An error occurred',
title: t('defaultErrorToastDescription'),
description: response.data.message, description: response.data.message,
status: 'error', status: 'error',
duration: 9000, duration: 9000,
@ -59,8 +63,8 @@ function Login() {
if (err.response.status === 401) { if (err.response.status === 401) {
dispatch(authLogout()); dispatch(authLogout());
toast({ toast({
title: 'Login Failed',
description: 'The wrong credentials were used.',
title: t('failedLoginToastTitle'),
description: t('failedLoginToastDescription'),
status: 'error', status: 'error',
duration: 9000, duration: 9000,
isClosable: true, isClosable: true,
@ -68,8 +72,8 @@ function Login() {
} }
} else { } else {
toast({ toast({
title: 'An error occurred',
description: 'Sorry, an error occurred on our side.',
title: t('errorToastTitle'),
description: t('defaultErrorToastDescription'),
status: 'error', status: 'error',
duration: 9000, duration: 9000,
isClosable: true, isClosable: true,
@ -95,23 +99,24 @@ function Login() {
id="contentFlex" id="contentFlex"
> >
<Heading size="xl" mb={6}> <Heading size="xl" mb={6}>
OurSejahtera Contact Tracing
{t('appTitle')}
</Heading> </Heading>
<Heading size="lg" mb={4}> <Heading size="lg" mb={4}>
Login
{t('login')}
</Heading> </Heading>
<TelegramLoginButton <TelegramLoginButton
dataOnauth={handleTelegramResponse} dataOnauth={handleTelegramResponse}
botName={process.env.REACT_APP_TELEGRAM_BOT_NAME} botName={process.env.REACT_APP_TELEGRAM_BOT_NAME}
/> />
<Divider mb={6} mt={6} /> <Divider mb={6} mt={6} />
<Text fontSize="sm">
<Trans i18nKey="loginPrivacyNotice">
<Text mb={6} fontSize="sm">
<b>Privacy notes:</b> <br /> <b>Privacy notes:</b> <br />
Telegram Login allows us to verify your identity, without collecting Telegram Login allows us to verify your identity, without collecting
any of your data. Telegram does NOT give us your phone number. The any of your data. Telegram does NOT give us your phone number. The
only piece of information stored on our server is your Telegram ID, only piece of information stored on our server is your Telegram ID,
this is an internal ID Number Telegram uses that is SEPARATE from your
Telegram Username.
this is an internal ID Number Telegram uses that is SEPARATE from
your Telegram Username.
<br /> <br />
<br /> <br />
All the code for this project is{' '} All the code for this project is{' '}
@ -125,6 +130,9 @@ function Login() {
, that means anyone, including you can audit and verify that your , that means anyone, including you can audit and verify that your
information is being handled securely. information is being handled securely.
</Text> </Text>
</Trans>
<Divider mb={6} />
<LanguageSwitcher />
</Flex> </Flex>
</Flex> </Flex>
); );

18
src/screens/ScannerScreen.js

@ -1,6 +1,7 @@
import { Button, Divider, Flex, useToast } from '@chakra-ui/react'; import { Button, Divider, Flex, useToast } from '@chakra-ui/react';
import axios from 'axios'; import axios from 'axios';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import QrReader from 'react-qr-reader'; import QrReader from 'react-qr-reader';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { Redirect, useHistory } from 'react-router-dom'; import { Redirect, useHistory } from 'react-router-dom';
@ -11,6 +12,7 @@ function Scanner() {
const toast = useToast(); const toast = useToast();
const history = useHistory(); const history = useHistory();
const dispatch = useDispatch(); const dispatch = useDispatch();
const [t] = useTranslation();
const [scanData, setScanData] = useState(null); const [scanData, setScanData] = useState(null);
@ -32,8 +34,7 @@ function Scanner() {
const hash = re.exec(scanData); const hash = re.exec(scanData);
if (hash) { if (hash) {
toast({ toast({
title: 'Checking QR code.',
description: "Hold on, we're checking this QR code.",
title: t('checkingQRToastTitle'),
status: 'info', status: 'info',
duration: 10000, duration: 10000,
isClosable: false, isClosable: false,
@ -51,14 +52,13 @@ function Scanner() {
if (res.data.loggedIn) { if (res.data.loggedIn) {
toast.closeAll(); toast.closeAll();
toast({ toast({
title: 'Contact Succesfully Logged',
title: t('contactLoggedToastTitle'),
status: 'info', status: 'info',
duration: 2000, duration: 2000,
}); });
} else { } else {
toast({ toast({
title: "You're not logged in!",
description: 'Please log in and try again!',
title: t('notLoggedInToastDescription'),
status: 'error', status: 'error',
duration: 2000, duration: 2000,
}); });
@ -70,7 +70,7 @@ function Scanner() {
.catch(e => { .catch(e => {
toast.closeAll(); toast.closeAll();
toast({ toast({
title: 'Bad Verification',
title: t('badVerificationToastTitle'),
status: 'error', status: 'error',
duration: 2000, duration: 2000,
}); });
@ -78,14 +78,14 @@ function Scanner() {
} else { } else {
toast.closeAll(); toast.closeAll();
toast({ toast({
title: 'Bad QR code',
title: t('badQRCodeToastTitle'),
status: 'error', status: 'error',
duration: 3000, duration: 3000,
isClosable: true, isClosable: true,
}); });
} }
} }
}, [scanData, dispatch, history, toast]);
}, [scanData, dispatch, history, toast, t]);
const isAuthenticated = useSelector(state => state.auth.isAuthenticated); const isAuthenticated = useSelector(state => state.auth.isAuthenticated);
const isCovidPositive = useSelector(state => state.covid.isCovidPositive); const isCovidPositive = useSelector(state => state.covid.isCovidPositive);
@ -135,7 +135,7 @@ function Scanner() {
history.push('/home'); history.push('/home');
}} }}
> >
Show my QR Code
{t('showQRCode')}
</Button> </Button>
</Flex> </Flex>
</Flex> </Flex>

9
src/screens/SuccessScreen.js

@ -1,8 +1,10 @@
import { Button, Flex, Heading, Text } from '@chakra-ui/react'; import { Button, Flex, Heading, Text } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
function Success() { function Success() {
const history = useHistory(); const history = useHistory();
const [t] = useTranslation();
return ( return (
<Flex <Flex
@ -20,16 +22,13 @@ function Success() {
rounded={6} rounded={6}
> >
<Heading>Success!</Heading> <Heading>Success!</Heading>
<Text fontSize="lg">
We have succesfully saved your contact! Stay safe out there, and let
others scan your QR code too!
</Text>
<Text fontSize="lg">{t('contactSavedParagraph')}</Text>
<Button <Button
onClick={() => { onClick={() => {
history.push('/home'); history.push('/home');
}} }}
> >
Return home
{t('returnHomeButtonLabel')}
</Button> </Button>
</Flex> </Flex>
</Flex> </Flex>

12
src/screens/VerifyScreen.js

@ -1,12 +1,14 @@
import { Flex, Text } from '@chakra-ui/react'; import { Flex, Text } from '@chakra-ui/react';
import axios from 'axios'; import axios from 'axios';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
function Verify(props) { function Verify(props) {
const verifyID = props.match.params.id; const verifyID = props.match.params.id;
const [verifyError, setVerifyError] = useState(false); const [verifyError, setVerifyError] = useState(false);
const history = useHistory(); const history = useHistory();
const [t] = useTranslation();
useEffect(() => { useEffect(() => {
if (verifyError) return; if (verifyError) return;
@ -32,14 +34,8 @@ function Verify(props) {
}); });
}, [verifyError, history, verifyID]); }, [verifyError, history, verifyID]);
const errorMessage = (
<Text>
An error has occured verifying you. Please try scanning the QR code again?
</Text>
);
const loadingMessage = (
<Text>We are currently verifying you. Please wait.</Text>
);
const errorMessage = <Text>{t('verifyingErrorParagraph')}</Text>;
const loadingMessage = <Text>{t('verifyingParagraph')}</Text>;
return ( return (
<Flex <Flex

37
yarn.lock

@ -1091,7 +1091,7 @@
dependencies: dependencies:
regenerator-runtime "^0.13.4" regenerator-runtime "^0.13.4"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.9.2":
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.14.5", "@babel/runtime@^7.9.2":
version "7.14.8" version "7.14.8"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.8.tgz#7119a56f421018852694290b9f9148097391b446" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.8.tgz#7119a56f421018852694290b9f9148097391b446"
integrity sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg== integrity sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg==
@ -4301,6 +4301,11 @@ cosmiconfig@^7.0.0:
path-type "^4.0.0" path-type "^4.0.0"
yaml "^1.10.0" yaml "^1.10.0"
country-flag-icons@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/country-flag-icons/-/country-flag-icons-1.4.0.tgz#68a55441dbe98a7f028d4b00c1dfef3b9f83b7cd"
integrity sha512-tIeh/IZbm3G7O1ocNuEckmd1jkABkJzCPhQm+rBgJXkz1L+lKfwVY/rAy+ih06zGD4kagrZ+1eOndga09y8UHw==
create-ecdh@^4.0.0: create-ecdh@^4.0.0:
version "4.0.4" version "4.0.4"
resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e"
@ -6336,6 +6341,13 @@ html-minifier-terser@^5.0.1:
relateurl "^0.2.7" relateurl "^0.2.7"
terser "^4.6.3" terser "^4.6.3"
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@4.5.0: html-webpack-plugin@4.5.0:
version "4.5.0" version "4.5.0"
resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-4.5.0.tgz#625097650886b97ea5dae331c320e3238f6c121c" resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-4.5.0.tgz#625097650886b97ea5dae331c320e3238f6c121c"
@ -6443,6 +6455,13 @@ human-signals@^1.1.1:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
i18next@^20.3.5:
version "20.3.5"
resolved "https://registry.yarnpkg.com/i18next/-/i18next-20.3.5.tgz#14308b79a3f1cafb24fdcd8e182d3673baf1e979"
integrity sha512-//MGeU6n4TencJmCgG+TCrpdgAD/NDEU/KfKQekYbJX6QV7sD/NjWQdVdBi+bkT0snegnSoB7QhjSeatrk3a0w==
dependencies:
"@babel/runtime" "^7.12.0"
iconv-lite@0.4.24: iconv-lite@0.4.24:
version "0.4.24" version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@ -9913,12 +9932,13 @@ react-focus-lock@2.5.0:
use-callback-ref "^1.2.1" use-callback-ref "^1.2.1"
use-sidecar "^1.0.1" use-sidecar "^1.0.1"
react-icons@^3.0.0:
version "3.11.0"
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-3.11.0.tgz#2ca2903dfab8268ca18ebd8cc2e879921ec3b254"
integrity sha512-JRgiI/vdF6uyBgyZhVyYJUZAop95Sy4XDe/jmT3R/bKliFWpO/uZBwvSjWEdxwzec7SYbEPNPck0Kff2tUGM2Q==
react-i18next@^11.11.4:
version "11.11.4"
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.11.4.tgz#f6f9a1c827e7a5271377de2bf14db04cb1c9e5ce"
integrity sha512-ayWFlu8Sc7GAxW1PzMaPtzq+yiozWMxs0P1WeITNVzXAVRhC0Httkzw/IiODBta6seJRBCLrtUeFUSXhAIxlRg==
dependencies: dependencies:
camelcase "^5.0.0"
"@babel/runtime" "^7.14.5"
html-parse-stringify "^3.0.1"
react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
version "16.13.1" version "16.13.1"
@ -12019,6 +12039,11 @@ vm-browserify@^1.0.1:
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
void-elements@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
integrity sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=
w3c-hr-time@^1.0.2: w3c-hr-time@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"

Loading…
Cancel
Save