diff --git a/CHANGELOG.md b/CHANGELOG.md index 19092d9..0423668 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [Unreleased] +- Enable translation (#22) +- Reformat nickname (#20) +- Add color to the diagram (#20) ## [1.0.0] 2023-08-09 diff --git a/package-lock.json b/package-lock.json index 0d600ce..62ed4c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,9 +12,11 @@ "bootstrap-icons": "^1.10.5", "file-saver": "^2.0.5", "gojs": "^2.3.6", + "i18next": "^23.11.2", "lodash": "^4.17.21", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-i18next": "^14.1.0", "reactstrap": "^9.1.9", "yaml": "^2.3.1" }, @@ -1944,16 +1946,21 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", - "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz", + "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==", "dependencies": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/runtime/node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "node_modules/@babel/template": { "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", @@ -9492,6 +9499,14 @@ "node": ">=12" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/html-webpack-plugin": { "version": "5.5.1", "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.1.tgz", @@ -9636,6 +9651,28 @@ "node": ">=10.17.0" } }, + "node_modules/i18next": { + "version": "23.11.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.11.2.tgz", + "integrity": "sha512-qMBm7+qT8jdpmmDw/kQD16VpmkL9BdL+XNAK5MNbNFaf1iQQq35ZbPrSlqmnNPOSUY4m342+c0t0evinF5l7sA==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -15565,6 +15602,27 @@ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.1.tgz", "integrity": "sha512-xTYf9zFim2pEif/Fw16dBiXpe0hoy5PxcD8+OwBnTtNLfIm3g6WxhKNurY+6OmdH1u6Ta/W/Vl6vjbYP1MFnDg==" }, + "node_modules/react-i18next": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.0.tgz", + "integrity": "sha512-3KwX6LHpbvGQ+sBEntjV4sYW3Zovjjl3fpoHbUwSgFHf0uRBcbeCBLR5al6ikncI5+W0EFb71QXZmfop+J6NrQ==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -15780,7 +15838,8 @@ "node_modules/regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true }, "node_modules/regenerator-transform": { "version": "0.15.1", @@ -17887,6 +17946,14 @@ "node": ">= 0.8" } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", diff --git a/package.json b/package.json index 036e0ba..d7dbfbd 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,11 @@ "bootstrap-icons": "^1.10.5", "file-saver": "^2.0.5", "gojs": "^2.3.6", + "i18next": "^23.11.2", "lodash": "^4.17.21", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-i18next": "^14.1.0", "reactstrap": "^9.1.9", "yaml": "^2.3.1" }, diff --git a/src/components/App.test.tsx b/src/components/App.test.tsx index e1eac46..21f3826 100644 --- a/src/components/App.test.tsx +++ b/src/components/App.test.tsx @@ -1,9 +1,26 @@ import { render, screen } from '@testing-library/react'; import App from './App'; +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; import * as go from 'gojs'; go.Palette.useDOM(false); +i18n.use(initReactI18next).init({ + fallbackLng: ['dev'], + resources: { + dev: { + translation: { + 'header.family': '{{name}} Family', + 'header.family_general': 'Family Grid' + }, + }, + }, + interpolation: { + escapeValue: false, + }, +}); + const trees = [{ id: 'satyr', code: '', diff --git a/src/components/App.tsx b/src/components/App.tsx index 6a36091..0359b15 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,5 +1,13 @@ -import { Button, Container, Form, FormGroup, Input, Label } from 'reactstrap'; -import { useMemo, useState } from 'react'; +import { + Button, + ButtonGroup, + Container, + Form, + FormGroup, + Input, + Label, +} from 'reactstrap'; +import { useEffect, useMemo, useState } from 'react'; import { parse, stringify } from 'yaml'; import { saveAs } from 'file-saver'; import { Person } from '../family.interface'; @@ -17,6 +25,7 @@ import { unrichTreeData, } from '../family.util'; import { useCache } from '../useCache'; +import { useTranslation } from 'react-i18next'; import Footer from './Footer'; interface AppProps { @@ -36,6 +45,14 @@ function App(props: AppProps) { const [modalSpouse, setModalSpouse] = useState(null as Person | null); const [treeYaml, setTreeYaml] = useState(''); + const { t, i18n } = useTranslation(); + const [language, setLanguage] = useCache('language', i18n.language); + i18n.on('languageChanged', (lang: string) => setLanguage(lang)); + useEffect(() => { + i18n.changeLanguage(language); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + const languages = i18n.languages ? [...i18n.languages].sort() : []; + const [showModalAddTree, setShowModalAddTree] = useState(false); const toggleModalAddTree = () => setShowModalAddTree(!showModalAddTree); const openModalAddTree = () => { @@ -133,6 +150,21 @@ function App(props: AppProps) { >
+ + + {languages.map(lang => ( + + ))} + + setSplitValue(!split)} /> @@ -152,7 +184,7 @@ function App(props: AppProps) { onChange={() => setHideCode(!hidePersonCode)} /> @@ -163,7 +195,7 @@ function App(props: AppProps) { onChange={() => setHideIg(!hidePersonIg)} /> @@ -174,25 +206,25 @@ function App(props: AppProps) { onChange={() => setEditModeValue(!editMode)} /> {' '} {' '} {' '} @@ -201,10 +233,10 @@ function App(props: AppProps) { onClick={() => openModalEditYaml()} color="warning" > - Edit tree + {t('config.editTree')} {' '} {' '} diff --git a/src/components/Family.tsx b/src/components/Family.tsx index 1c62516..4c0edd1 100644 --- a/src/components/Family.tsx +++ b/src/components/Family.tsx @@ -4,6 +4,7 @@ import AppContext from './AppContext'; import FamilyGrid from './FamilyGrid'; import FamilyDiagram from './FamilyDiagram'; import { explodeTrees, idAsNickName } from '../family.util'; +import { useTranslation } from 'react-i18next'; interface FamilyProps { trees: Person[]; @@ -15,9 +16,11 @@ export default function Family(props: FamilyProps) { } function SplitFamilies({ trees, ...props }: FamilyProps) { + const { t } = useTranslation(); const people = explodeTrees(trees).filter( person => person.marriages.length !== 0 ); + const getName = (p: Person) => p.name ?? idAsNickName(p.id); return ( <> @@ -25,7 +28,7 @@ function SplitFamilies({ trees, ...props }: FamilyProps) {

- {tree.name ?? idAsNickName(tree.id)} Family + {t('header.family', { name: getName(tree) })}

@@ -36,10 +39,12 @@ function SplitFamilies({ trees, ...props }: FamilyProps) { } function BigFamily({ trees, ...props }: FamilyProps) { + const { t } = useTranslation(); + return (
-

Family Grid

+

{t('header.family_general')}

diff --git a/src/components/FamilyGrid.tsx b/src/components/FamilyGrid.tsx index 296d728..606f0f2 100644 --- a/src/components/FamilyGrid.tsx +++ b/src/components/FamilyGrid.tsx @@ -2,6 +2,7 @@ import { ChangeEvent, useContext } from 'react'; import { Input, Table } from 'reactstrap'; import { Person } from '../family.interface'; import { explodeTrees, idAsNickName } from '../family.util'; +import { useTranslation } from 'react-i18next'; import AppContext from './AppContext'; interface FamilyGridProps { @@ -10,21 +11,22 @@ interface FamilyGridProps { export default function FamilyGrid({ trees }: FamilyGridProps) { const { hidePersonCode, hidePersonIg } = useContext(AppContext); + const { t } = useTranslation(); return ( - - - - - + + + + + diff --git a/src/components/ModalAddChild.tsx b/src/components/ModalAddChild.tsx index 09a5635..27c59b4 100644 --- a/src/components/ModalAddChild.tsx +++ b/src/components/ModalAddChild.tsx @@ -10,6 +10,7 @@ import { ModalHeader, } from 'reactstrap'; import { useContext, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { Marriage, Person } from '../family.interface'; import AppContext from './AppContext'; @@ -33,6 +34,7 @@ function ModalAddChild({ const { treeMap, upsertPerson } = useContext(AppContext); const [child, setChild] = useState(''); const [childError, setChildError] = useState(''); + const { t } = useTranslation(); const marriedPeople = Object.values(treeMap).filter( person => person.marriages.length > 0 @@ -60,7 +62,7 @@ function ModalAddChild({ setChildError(''); if (Object.keys(treeMap).includes(value)) { - setChildError(value + ' is already taken'); + setChildError(t('error.alreadyTaken', { value: value })); } }; @@ -90,17 +92,17 @@ function ModalAddChild({ return ( - Add a child + {t('config.label.addChild')} - + - + {marriedPeople.map(person => ( {person && ( - + - + {person.marriages.map(marriage => ( diff --git a/src/components/ModalAddSpouse.tsx b/src/components/ModalAddSpouse.tsx index 40710c4..c79b7f6 100644 --- a/src/components/ModalAddSpouse.tsx +++ b/src/components/ModalAddSpouse.tsx @@ -10,6 +10,7 @@ import { ModalHeader, } from 'reactstrap'; import { useContext, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { Marriage, Person } from '../family.interface'; import AppContext from './AppContext'; @@ -29,6 +30,7 @@ function ModalAddSpouse({ const { treeMap, upsertPerson } = useContext(AppContext); const [spouse, setSpouse] = useState(''); const [spouseError, setSpouseError] = useState(''); + const { t } = useTranslation(); const people = Object.values(treeMap); @@ -44,7 +46,7 @@ function ModalAddSpouse({ setSpouseError(''); if (Object.keys(treeMap).includes(value)) { - setSpouseError(value + ' is already taken'); + setSpouseError(t('error.alreadyTaken', { value: value })); } }; @@ -95,7 +97,7 @@ function ModalAddSpouse({ diff --git a/src/components/ModalAddTree.tsx b/src/components/ModalAddTree.tsx index 3a3d86f..be70960 100644 --- a/src/components/ModalAddTree.tsx +++ b/src/components/ModalAddTree.tsx @@ -10,6 +10,7 @@ import { ModalHeader, } from 'reactstrap'; import { useContext, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { Marriage, Person } from '../family.interface'; import AppContext from './AppContext'; @@ -22,6 +23,7 @@ function ModalAddTree({ isOpen, toggle }: ModalAddTreeProps) { const { treeMap, upsertPerson } = useContext(AppContext); const [child, setChild] = useState(''); const [childError, setChildError] = useState(''); + const { t } = useTranslation(); const handleChildChange = (event: React.ChangeEvent) => { const value = event.target.value; @@ -29,7 +31,7 @@ function ModalAddTree({ isOpen, toggle }: ModalAddTreeProps) { setChildError(''); if (Object.keys(treeMap).includes(value)) { - setChildError(value + ' is already taken'); + setChildError(t('error.alreadyTaken', { value: value })); } }; @@ -51,14 +53,14 @@ function ModalAddTree({ isOpen, toggle }: ModalAddTreeProps) { return ( - Add a tree + {t('config.label.addTree')} - + diff --git a/src/components/ModalDeletePerson.tsx b/src/components/ModalDeletePerson.tsx index db12fa1..07ee469 100644 --- a/src/components/ModalDeletePerson.tsx +++ b/src/components/ModalDeletePerson.tsx @@ -9,6 +9,7 @@ import { ModalHeader, } from 'reactstrap'; import { useContext } from 'react'; +import { useTranslation } from 'react-i18next'; import { Person } from '../family.interface'; import AppContext from './AppContext'; @@ -26,6 +27,7 @@ function ModalDeletePerson({ toggle, }: ModalDeletePersonProps) { const { treeMap, deletePerson } = useContext(AppContext); + const { t } = useTranslation(); const people = Object.values(treeMap); const handlePersonChange = (event: React.ChangeEvent) => { @@ -43,17 +45,19 @@ function ModalDeletePerson({ return ( - Delete a person + + {t('config.label.deletePerson')} + - + - + {people.map(person => ( diff --git a/src/components/ModalEditYaml.tsx b/src/components/ModalEditYaml.tsx index 1117699..e3b31e1 100644 --- a/src/components/ModalEditYaml.tsx +++ b/src/components/ModalEditYaml.tsx @@ -21,6 +21,7 @@ import { import { Person } from '../family.interface'; import { enrichTreeData } from '../family.util'; import { parse } from 'yaml'; +import { useTranslation } from 'react-i18next'; import AppContext from './AppContext'; import FamilyDiagram from './FamilyDiagram'; @@ -43,6 +44,7 @@ function ModalEditYaml({ const { setTreesValue } = useContext(AppContext); const [yamlError, setYamlError] = useState(''); const [trees, setTrees] = useState([] as Person[]); + const { t } = useTranslation(); const deferredTree = useDeferredValue(trees); const deferredTreeYaml = useDeferredValue(treeYaml); const loading = deferredTreeYaml !== treeYaml; @@ -85,10 +87,12 @@ function ModalEditYaml({ return ( - Edit tree + {t('config.editTree')} - Tree preview + + {t('config.label.treePreview')} + @@ -96,7 +100,7 @@ function ModalEditYaml({ - + diff --git a/src/i18n.tsx b/src/i18n.tsx new file mode 100644 index 0000000..7d60566 --- /dev/null +++ b/src/i18n.tsx @@ -0,0 +1,14 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import en from './lang-en.yml'; +import id from './lang-id.yml'; + +i18n.use(initReactI18next).init({ + fallbackLng: ['en', 'id'], + resources: { en, id }, + interpolation: { + escapeValue: false, + }, +}); + +export default i18n; diff --git a/src/index.tsx b/src/index.tsx index ec5b1fd..270f53a 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,6 +4,7 @@ import App from './components/App'; import { Person } from './family.interface'; import { enrichTreeData } from './family.util'; import rawFamilyData from './data.yml'; +import './i18n'; import 'bootstrap/dist/css/bootstrap.min.css'; import 'bootstrap-icons/font/bootstrap-icons.min.css'; diff --git a/src/lang-en.yml b/src/lang-en.yml new file mode 100644 index 0000000..fe4edda --- /dev/null +++ b/src/lang-en.yml @@ -0,0 +1,41 @@ +translation: + config.addChild: Add Child + config.addSpouse: Add Spouse + config.addTree: Add Family + config.button.apply: Apply! + config.button.delete: Delete! + config.button.submit: Submit! + config.deletePerson: Delete Person + config.editMode: Edit Mode + config.editTree: Edit Family Tree + config.export: Export + config.import: Import + config.label.addChild: Add a Child + config.label.addSpouse: Add a Spouse + config.label.addTree: Add a Family + config.label.child: Child + config.label.deletePerson: Delete a Person + config.label.editTree: Edit Tree + config.label.person: Person + config.label.selectChild: Select a Child + config.label.selectPerson: Select a Person + config.label.selectSpouse: Select a Spouse + config.label.spouse: Spouse + config.label.treePreview: Show Family Tree Diagram + config.placeholder.child: Insert the child unique nickname + config.placeholder.person: Insert the person unique nickname + config.placeholder.spouse: Insert the spouse unique nickname + config.showCode: Show Code + config.showIg: Show Instagram + config.splitFamily: Split Family + error.alreadyTaken: '{{value}} is already taken' + header.family: '{{name}} Family' + header.family_general: Family Grid + table.header.address: Address + table.header.birthdate: Birthdate + table.header.birthplace: Birthplace + table.header.code: Code + table.header.deathdate: Death Date + table.header.ig: Instagram + table.header.name: Name + table.header.phone: Phone diff --git a/src/lang-id.yml b/src/lang-id.yml new file mode 100644 index 0000000..d247655 --- /dev/null +++ b/src/lang-id.yml @@ -0,0 +1,41 @@ +translation: + config.addChild: Tambah Anak + config.addSpouse: Tambah Pasangan + config.addTree: Tambah Keluarga + config.button.apply: Terapkan! + config.button.delete: Hapus! + config.button.submit: Terapkan! + config.deletePerson: Hapus Orang + config.editMode: Mode Edit + config.editTree: Edit Pohon Keluarga + config.export: Expor + config.import: Impor + config.label.addChild: Tambah Anak + config.label.addSpouse: Tambah Pasangan + config.label.addTree: Tambah Keluarga + config.label.child: Anak + config.label.deletePerson: Hapus Orang + config.label.editTree: Edit Pohon Keluarga + config.label.person: Orang + config.label.selectChild: Pilih Anak + config.label.selectPerson: Pilih Orang + config.label.selectSpouse: Pilih Pasangan + config.label.spouse: Pasangan + config.label.treePreview: Tampilan Diagram Pohon Keluarga + config.placeholder.child: Masukkan nama panggilan anak + config.placeholder.person: Masukkan nama panggilan orang + config.placeholder.spouse: Masukkan nama panggilan pasangan + config.showCode: Tampilkan Kode + config.showIg: Tampilkan Instagram + config.splitFamily: Pisah Per Keluarga + error.alreadyTaken: '{{value}} sudah digunakan' + header.family: 'Keluarga {{name}}' + header.family_general: Family Grid + table.header.address: Alamat + table.header.birthdate: Tgl Lahir + table.header.birthplace: Tempat Lahir + table.header.code: Kode + table.header.deathdate: Tgl Kematian + table.header.ig: Instagram + table.header.name: Nama + table.header.phone: Telpon
NameBirthplaceBirthdatePhoneAddress{t('table.header.name')}{t('table.header.birthplace')}{t('table.header.birthdate')}{t('table.header.phone')}{t('table.header.address')}