Feat: implement i18n support

This commit is contained in:
Richard Wong 2024-07-01 19:21:15 +09:00
parent 2e29c70d17
commit 32363c9d7b
Signed by: richard
GPG Key ID: 72948FBB6D359A6D
10 changed files with 3275 additions and 1517 deletions

View File

@ -11,9 +11,13 @@
},
"dependencies": {
"@fontsource/montserrat": "^4.5.14",
"i18next": "^23.11.5",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-http-backend": "^2.5.2",
"react": "^18.2.0",
"react-checkbox-tree": "^1.8.0",
"react-dom": "^18.2.0",
"react-i18next": "^14.1.2",
"react-string-diff": "^0.2.0",
"underscore": "^1.13.6"
},

View File

@ -8,6 +8,15 @@ dependencies:
'@fontsource/montserrat':
specifier: ^4.5.14
version: 4.5.14
i18next:
specifier: ^23.11.5
version: 23.11.5
i18next-browser-languagedetector:
specifier: ^8.0.0
version: 8.0.0
i18next-http-backend:
specifier: ^2.5.2
version: 2.5.2
react:
specifier: ^18.2.0
version: 18.2.0
@ -17,6 +26,9 @@ dependencies:
react-dom:
specifier: ^18.2.0
version: 18.2.0(react@18.2.0)
react-i18next:
specifier: ^14.1.2
version: 14.1.2(i18next@23.11.5)(react-dom@18.2.0)(react@18.2.0)
react-string-diff:
specifier: ^0.2.0
version: 0.2.0(react@18.2.0)
@ -244,6 +256,13 @@ packages:
'@babel/helper-plugin-utils': 7.21.5
dev: true
/@babel/runtime@7.24.7:
resolution: {integrity: sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==}
engines: {node: '>=6.9.0'}
dependencies:
regenerator-runtime: 0.14.1
dev: false
/@babel/template@7.20.7:
resolution: {integrity: sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==}
engines: {node: '>=6.9.0'}
@ -808,6 +827,14 @@ packages:
resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
dev: true
/cross-fetch@4.0.0:
resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==}
dependencies:
node-fetch: 2.7.0
transitivePeerDependencies:
- encoding
dev: false
/cross-spawn@7.0.3:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'}
@ -1306,6 +1333,32 @@ packages:
function-bind: 1.1.1
dev: true
/html-parse-stringify@3.0.1:
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
dependencies:
void-elements: 3.1.0
dev: false
/i18next-browser-languagedetector@8.0.0:
resolution: {integrity: sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==}
dependencies:
'@babel/runtime': 7.24.7
dev: false
/i18next-http-backend@2.5.2:
resolution: {integrity: sha512-+K8HbDfrvc1/2X8jpb7RLhI9ZxBDpx3xogYkQwGKlWAUXLSEGXzgdt3EcUjLlBCdMwdQY+K+EUF6oh8oB6rwHw==}
dependencies:
cross-fetch: 4.0.0
transitivePeerDependencies:
- encoding
dev: false
/i18next@23.11.5:
resolution: {integrity: sha512-41pvpVbW9rhZPk5xjCX2TPJi2861LEig/YRhUkY+1FQ2IQPS0bKUDYnEqY8XPPbB48h1uIwLnP9iiEfuSl20CA==}
dependencies:
'@babel/runtime': 7.24.7
dev: false
/ignore@5.2.4:
resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
engines: {node: '>= 4'}
@ -1558,6 +1611,18 @@ packages:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
dev: true
/node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
dependencies:
whatwg-url: 5.0.0
dev: false
/node-releases@2.0.10:
resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==}
dev: true
@ -1733,6 +1798,26 @@ packages:
scheduler: 0.23.0
dev: false
/react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-FSIcJy6oauJbGEXfhUgVeLzvWBhIBIS+/9c6Lj4niwKZyGaGb4V4vUbATXSlsHJDXXB+ociNxqFNiFuV1gmoqg==}
peerDependencies:
i18next: '>= 23.2.3'
react: '>= 16.8.0'
react-dom: '*'
react-native: '*'
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
dependencies:
'@babel/runtime': 7.24.7
html-parse-stringify: 3.0.1
i18next: 23.11.5
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
@ -1758,6 +1843,10 @@ packages:
loose-envify: 1.4.0
dev: false
/regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
dev: false
/regexp.prototype.flags@1.5.0:
resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==}
engines: {node: '>= 0.4'}
@ -1929,6 +2018,10 @@ packages:
engines: {node: '>=4'}
dev: true
/tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
dev: false
/type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@ -2011,6 +2104,22 @@ packages:
fsevents: 2.3.2
dev: true
/void-elements@3.1.0:
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
engines: {node: '>=0.10.0'}
dev: false
/webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
dev: false
/whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
dependencies:
tr46: 0.0.3
webidl-conversions: 3.0.1
dev: false
/which-boxed-primitive@1.0.2:
resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==}
dependencies:

View File

@ -0,0 +1,22 @@
{
"main": {
"title": "Scripture Memory Tester",
"pick_lang": "Pick Language",
"pick_num_verses": "Pick Number of Verses:",
"num_verses_tested": "Number of Verses Tested:",
"note_num_verses": "(It will only give you as many verses as there are in selected packs)",
"set_shuffle": "Set Shuffle:",
"note_set_shuffle": "(Otherwise cards will appear in sequential order)",
"hide_reference": "Set Hide Reference:",
"note_hide_reference": "(If you also want to test the verse reference)",
"pick_pack": "Pick Your Packs:",
"shuffle_card": "Shuffle Cards:",
"verses": "Verses:"
},
"verse_validator": {
"input_reference": "Input Verse Reference:",
"input_chapter_title": "Input Chapter Title:",
"input_title": "Input Title:",
"input_verse": "Input Verse:"
}
}

View File

@ -0,0 +1,22 @@
{
"main": {
"title": "Scripture Memory Tester (kr)",
"pick_lang": "Pick Language (kr)",
"pick_num_verses": "Pick Number of Verses: (kr)",
"num_verses_tested": "Number of Verses Tested:",
"note_num_verses": "(It will only give you as many verses as there are in selected packs)",
"set_shuffle": "Set Shuffle:",
"note_set_shuffle": "(Otherwise cards will appear in sequential order)",
"hide_reference": "Set Hide Reference:",
"note_hide_reference": "(If you also want to test the verse reference)",
"pick_pack": "Pick Your Packs:",
"shuffle_card": "Shuffle Cards:",
"verses": "Verses:"
},
"verse_validator": {
"input_reference": "Input Verse Reference: (KR)",
"input_chapter_title": "Input Chapter Title: (KR)",
"input_title": "Input Title: (KR)",
"input_verse": "Input Verse: (KR)"
}
}

View File

@ -3,15 +3,18 @@ Implemented features:
- read keys from json
- create checklist from keys
*/
import VerseData from "./assets/verse.json"
import fullVerseData from "./assets/verse.json" // the actual verse json data file
import { useState } from "react";
import CheckboxTree from 'react-checkbox-tree';
import 'react-checkbox-tree/lib/react-checkbox-tree.css';
import _ from 'underscore';
import './VerseSampler.css'
import VerseValidator from "./VerseValidator";
import { useTranslation } from 'react-i18next';
import logo from './assets/droplet.svg';
import { Suspense } from "react";
const GenerateTestList = ({ packs, testCount, toShuffle, toHideReference}) => {
const GenerateTestList = ({ VerseData, packs, testCount, toShuffle, toHideReference}) => {
let testList = packs.reduce(
// grab all elements included checked in "packs"
(accumulator, currentValue) => accumulator.concat(VerseData[currentValue]),
@ -110,10 +113,41 @@ const CheckboxWidget = ({checked, expanded, setChecked, setExpanded}) => {
);
}
// loadCustomData
const loadCustomData = (language) => {
let data;
console.log(language)
switch (language) {
case 'kn':
data = fullVerseData.kn;
break;
case 'en':
default:
data = fullVerseData.en;
break;
}
return data;
};
function Page() {
// setup i18 for function
const { t, i18n } = useTranslation();
// load VerseData json data file
const [VerseData, setVerseData] = useState(loadCustomData(i18n.language));
// function hook to change language
// updates both i18n language and also the VerseData state variable
const changeLanguage = (lng) => {
i18n.changeLanguage(lng);
setVerseData(loadCustomData(i18n.language));
};
function App() {
// create checklist array for pack selection
const packList = Object.keys(VerseData);
// return a list of packObj's
@ -178,10 +212,13 @@ function App() {
return (
<div className="App">
<h1>Scripture Memory Tester</h1>
<h2>Pick Number of Verses:</h2>
<h1>{t('main.title')}</h1>
<h2>{t('main.pick_lang')}</h2>
<button type="button" onClick={() => changeLanguage('en')}>English</button>
<button type="button" onClick={() => changeLanguage('kn')}>Korean</button>
<h2>{t('main.pick_num_verses')}</h2>
<label className="test-count-box-label" htmlFor="testCountBox">
Number of Verses Tested:
{t('main.num_verses_tested')}
</label>
<input
className="test-count-box"
@ -192,7 +229,7 @@ function App() {
onChange={testCountChange}
/>
<p>(It will only give you as many verses as there are in selected packs)</p>
<p>{t('main.note_num_verses')}</p>
<h2>
Set Shuffle:
@ -202,26 +239,26 @@ function App() {
onChange={handleShuffleCheckboxChange}
/>
</h2>
<p>(Otherwise cards will appear in sequential order)</p>
<p>{t('main.note_set_shuffle')}</p>
<div>
{!toShuffle ?
<>
<h2>
Set Hide Reference:
{t('main.hide_reference')}
<input
type="checkbox"
checked={toHideReference}
onChange={handleHideReferenceCheckboxChange}
/>
</h2>
<p>(If you also want to test the verse reference)</p>
<p>{t('main.note_hide_reference')}</p>
</>:
<p></p>}
</div>
<h2>Pick Your Packs:</h2>
<h2>{t('main.pick_pack')}</h2>
<CheckboxWidget
checked={checked}
expanded={expanded}
@ -232,14 +269,15 @@ function App() {
<div key={refreshKey}>
{toShuffle ?
<>
<h2>Shuffle Cards:</h2>
<h2>{t('main.shuffle_card')}</h2>
<RefreshButton onClick={handleRefresh} />
</>:
<p></p>}
</div>
<h1>Verses:</h1>
<h1>{t('main.verses')}</h1>
<GenerateTestList
VerseData={VerseData}
packs={checked}
testCount={testCount}
toShuffle={toShuffle}
@ -253,4 +291,18 @@ function App() {
);
}
export default App
// loading component for suspense fallback
const Loader = () => (
<div className="App">
<img src={logo} className="App-logo" alt="logo" />
<div>loading...</div>
</div>
);
export default function App() {
return (
<Suspense fallback={<Loader />}>
<Page />
</Suspense>
);
}

View File

@ -1,11 +1,15 @@
import { useState } from "react";
import "./VerseValidator.css";
import { StringDiff } from "react-string-diff";
import { useTranslation } from 'react-i18next';
// function to render and handle logic of each of the cells
const VerseValidator = ({ element: { pack, title, chapterTitle, reference, verse } , toHideReference}) => { // useful use of destructuring here
// setup i18 for function
const { t } = useTranslation();
const [inputReference, setReference] = useState('')
const [referenceBool, setReferenceBool] = useState(false)
const [inputChapterTitle, setChapterTitle] = useState('')
@ -117,7 +121,7 @@ const VerseValidator = ({ element: { pack, title, chapterTitle, reference, verse
{toHideReference ? (
<div>
<label className="reference-label">
Input Verse Reference:
{t('verse_validator.input_reference')}
</label>
<textarea
className={`reference-box${referenceBool ? " correct" : ""}`}
@ -137,7 +141,7 @@ const VerseValidator = ({ element: { pack, title, chapterTitle, reference, verse
{chapterTitle && (
<div>
<label className="main-title-box-label">
Input Chapter Title:
{t('verse_validator.input_chapter_title')}
</label>
<textarea
className={`chapter-title-box${chapterTitleBool ? " correct" : ""}`}
@ -151,7 +155,7 @@ const VerseValidator = ({ element: { pack, title, chapterTitle, reference, verse
{/* input box for title */}
<label className="title-box-label">
Input Title:
{t('verse_validator.input_title')}
</label>
<textarea
className={`title-box${titleBool ? " correct" : ""}`}
@ -163,7 +167,7 @@ const VerseValidator = ({ element: { pack, title, chapterTitle, reference, verse
{/* input box for verse */}
<label className="verse-box-label">
Input Verse:
{t('verse_validator.input_verse')}
</label>
<textarea
className={`verse-box${verseBool ? " correct" : ""}`}

4
src/assets/droplet.svg Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 13.8C19 17.7764 15.866 21 12 21C8.13401 21 5 17.7764 5 13.8C5 12.8452 5.18069 11.9338 5.50883 11.1C6.54726 8.46135 12 3 12 3C12 3 17.4527 8.46135 18.4912 11.1C18.8193 11.9338 19 12.8452 19 13.8Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 522 B

File diff suppressed because it is too large Load Diff

27
src/i18n.js Normal file
View File

@ -0,0 +1,27 @@
import i18n from 'i18next';
import Backend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';
i18n
// load translation using http -> see /public/locales
// learn more: https://github.com/i18next/i18next-http-backend
.use(Backend)
// detect user language
// learn more: https://github.com/i18next/i18next-browser-languageDetector
// note: I disabled detector to force en as default
.use(LanguageDetector)
// pass the i18n instance to react-i18next.
.use(initReactI18next) // default ./locales/en/translation.json
// init i18next
// for all options read: https://www.i18next.com/overview/configuration-options
.init({
fallbackLng: 'en',
detection: { // adds some caching
order: ['querystring', 'cookie', 'localStorage', 'navigator', 'htmlTag', 'path', 'subdomain'],
caches: ['localStorage', 'cookie'],
},
debug: true,
});
export default i18n;

View File

@ -4,6 +4,9 @@ import ReactDOM from 'react-dom/client'
import App from './VerseSampler.jsx'
// import App from './MultiCheckbox.jsx'
// import Widget from './ReactCheckboxTree.jsx'
// import i18n (needs to be bundled ;))
import './i18n';
import './index.css'
import "@fontsource/montserrat"