diff --git a/README.md b/README.md index 5bba52d..2556166 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ For Production: - [ ] Redux-saga - [x] Jest - [x] Axios -- [x] I18n [Not completed] +- [x] I18n - [x] React-router - [x] Alias - [x] Hot reload @@ -139,6 +139,43 @@ For Production: _- You should use [BEM](http://getbem.com/) to write css without conflict_ +- Using i18n + + - Create new json file at `src/locales/resources/.json` + - Add content follow this format into json file + ```javascript + { + "en": { + "name": "Name" + }, + "vi": { + "name": "Tên" + } + } + ``` + - update `src/locales/resources/index.ts` like this: + + ```javascript + /* + * you can use other name instead `user` + * this name will be used as path to key + */ + import user from './.json + + const mergeResource: IResource = { + ..., // others json + user + }; + ``` + + - Now inside any where, you can access to key like this: + + ```javascript + const { t } = useTranslation() + + t('user.name') will be render "Name" for `en` and "Tên" for `vi` + ``` + --- ## Tips diff --git a/config/@types/index.d.ts b/config/@types/index.d.ts index 3eb7fcc..38c8e75 100644 --- a/config/@types/index.d.ts +++ b/config/@types/index.d.ts @@ -15,7 +15,7 @@ declare module '*.png' { } declare module '*.json' { - const content: string; + const content: any; export default content; } diff --git a/public/static/images/icon/arrow-down.svg b/public/static/images/icon/arrow-down.svg new file mode 100644 index 0000000..7d54372 --- /dev/null +++ b/public/static/images/icon/arrow-down.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/static/images/icon/en.svg b/public/static/images/icon/en.svg new file mode 100644 index 0000000..cf79b12 --- /dev/null +++ b/public/static/images/icon/en.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/static/images/icon/vi.svg b/public/static/images/icon/vi.svg new file mode 100644 index 0000000..ee0866c --- /dev/null +++ b/public/static/images/icon/vi.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/select/index.tsx b/src/components/select/index.tsx new file mode 100644 index 0000000..44dac45 --- /dev/null +++ b/src/components/select/index.tsx @@ -0,0 +1,62 @@ +import useOutsideClick from '@/hooks/useOutsideClick'; +import React, { useRef, useState } from 'react'; +import ArrowDown from '@/static/images/icon/arrow-down.svg'; + +interface IOption { + value: any; + label: any; +} + +interface IProps { + options: Array; + width?: number; + onChange?: (option: IOption) => void; + className?: string; +} + +const Select: React.FC = ({ + options, + width = 240, + onChange, + className, +}) => { + const [show, setShow] = useState(false); + const [option, setOption] = useState(options[0]); + const selectRef = useRef(null); + useOutsideClick(selectRef, () => { + show === true && setShow(false); + }); + + function handleSelectDropdown(option: IOption) { + const opt = options.find(opt => opt.value === option.value); + + typeof onChange === 'function' && onChange(opt); + setOption(opt); + setShow(false); + } + return ( + + setShow(true)} className="dropdown-select"> + {option.label} + + + + {options.map(opt => ( + handleSelectDropdown(opt)} + className="dropdown-item" + > + {opt.label} + + ))} + + + ); +}; + +export default Select; diff --git a/src/components/select/style.scss b/src/components/select/style.scss new file mode 100644 index 0000000..3cdc8d9 --- /dev/null +++ b/src/components/select/style.scss @@ -0,0 +1,64 @@ +.dropdown { + color: $primary-color; + width: 100%; + position: relative; + border-radius: 8px; + .dropdown-caret { + width: 12px; + fill: $primary-color; + + &.up { + transform: rotate(180deg); + } + } + .dropdown-select { + background-color: white; + box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.1); + padding: 1rem; + border-radius: inherit; + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + } + .dropdown-select * { + pointer-events: none; + } + .dropdown-list { + position: absolute; + top: 100%; + left: 0; + right: 0; + margin-top: 0.4rem; + background-color: white; + box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.1); + padding: 1rem; + border-radius: 8px; + display: none; + &::before { + content: ''; + height: 1rem; + position: absolute; + top: 0; + left: 0; + right: 0; + background-color: transparent; + transform: translateY(-100%); + } + &.show { + display: block; + } + + .dropdown-item { + padding: 1rem; + color: #47536b; + transition: all 0.25s ease; + border-radius: 8px; + cursor: pointer; + &:hover { + color: $primary-color; + background-color: #f1fbff; + } + } + } +} diff --git a/src/features/home/index.tsx b/src/features/home/index.tsx index c51f403..c06f4b5 100644 --- a/src/features/home/index.tsx +++ b/src/features/home/index.tsx @@ -1,7 +1,13 @@ import React from 'react'; +import { useTranslation } from 'react-i18next'; import { ITag } from './types'; import HomeTag from './components/tag'; +import Select from '@/components/select'; +import i18n from '@/locales/i18n'; + import Banner from '@/static/images/banner.png'; +import ViIcon from '@/static/images/icon/vi.svg'; +import EnIcon from '@/static/images/icon/en.svg'; const keywords: Array = [ { label: 'React.js' }, @@ -12,23 +18,47 @@ const keywords: Array = [ { color: '#e94949', label: 'react-router' }, { color: '#bf4080', label: 'sass' }, { color: '#764abc', label: 'redux-thunk' }, - { color: '#2b037a', label: 'pm2' }, ]; +const languageOptions = [ + { + label: ( + + Vietnamese + + ), + value: 'vi', + }, + { + label: ( + + + English + + ), + value: 'en', + }, +]; + const Home: React.FC = () => { + const { t } = useTranslation(); return ( - - React-Typescript-Webpack was config with React, Typescript and Webpack - without CRA. Faster to start your next react project. - + { + i18n.changeLanguage(option.value); + }} + options={languageOptions} + /> + {t('home.title')} - Keywords: + {t('home.keywords')}: {keywords.map(key => ( ))} @@ -36,7 +66,8 @@ const Home: React.FC = () => { - Created with by 👻 Aldenn + {t('home.created_by')} 👻{' '} + Aldenn diff --git a/src/features/home/style.scss b/src/features/home/style.scss index c31064f..dd88f17 100644 --- a/src/features/home/style.scss +++ b/src/features/home/style.scss @@ -4,9 +4,19 @@ align-items: center; justify-content: center; text-align: center; + .select-language { + margin: 12px auto; + .lang-item { + display: flex; + align-items: center; + } + } .banner { margin-bottom: 20px; + @include s-mobile { + width: 100%; + } } .title { diff --git a/src/layouts/footer/index.tsx b/src/layouts/footer/index.tsx index 1e383ad..6d99345 100644 --- a/src/layouts/footer/index.tsx +++ b/src/layouts/footer/index.tsx @@ -4,10 +4,11 @@ import { selectDisplayLayout } from '@/store/slices/layoutSlice'; import Github from '@/static/images/icon/github.svg'; import LinkedIn from '@/static/images/icon/linkedin.svg'; import StackOverflow from '@/static/images/icon/stack-overflow.svg'; +import { useTranslation } from 'react-i18next'; const Footer: React.FC = () => { + const { t } = useTranslation(); const { footer } = useSelector(selectDisplayLayout); - if (!footer) { return null; } @@ -36,7 +37,7 @@ const Footer: React.FC = () => { - Copyright (c) 2021 by Aldenn + {t('footer.copy_right')} ); }; diff --git a/src/locales/en/home.json b/src/locales/en/home.json deleted file mode 100644 index dc3232a..0000000 --- a/src/locales/en/home.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "title": "React-Typescript-Webpack was config with React, Typescript and Webpack without CRA. Faster to start your next react project.", - "keywords": "Keywords", - "created_by": "Created with by", - "copy_right": "Copyright (c) 2021 by Aldenn" -} diff --git a/src/locales/i18n.ts b/src/locales/i18n.ts index 1af61ff..2fc315e 100644 --- a/src/locales/i18n.ts +++ b/src/locales/i18n.ts @@ -1,29 +1,17 @@ import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; -import homeEn from './en/home.json'; -import homeVi from './vi/home.json'; +import resources, { langs } from './resources'; +const isProduction = process.env.NODE_ENV === 'production'; -export const translationsJson = { - en: { - home: homeEn, +i18n.use(initReactI18next).init({ + resources, + fallbackLng: ['en'], + supportedLngs: langs, + debug: !isProduction, + interpolation: { + escapeValue: false, }, - vi: { - home: homeVi, - }, -}; - -i18n - .use(initReactI18next) // passes i18n down to react-i18next - .init({ - resources: translationsJson, - defaultNS: 'en', - lng: 'en', - fallbackLng: 'en', - - interpolation: { - escapeValue: false, - }, - }); +}); export default i18n; diff --git a/src/locales/languages.ts b/src/locales/languages.ts new file mode 100644 index 0000000..737f4d1 --- /dev/null +++ b/src/locales/languages.ts @@ -0,0 +1,4 @@ +export default { + vi: 'Vietnamese', + en: 'English', +}; diff --git a/src/locales/resources/footer.json b/src/locales/resources/footer.json new file mode 100644 index 0000000..1c94455 --- /dev/null +++ b/src/locales/resources/footer.json @@ -0,0 +1,8 @@ +{ + "en": { + "copy_right": "Copyright (c) 2021 by Aldenn" + }, + "vi": { + "copy_right": "Bản quyền (c) 2021 bởi Aldenn" + } +} diff --git a/src/locales/resources/home.json b/src/locales/resources/home.json new file mode 100644 index 0000000..db14eea --- /dev/null +++ b/src/locales/resources/home.json @@ -0,0 +1,14 @@ +{ + "en": { + "title": "React-Typescript-Webpack was config with React, Typescript and Webpack without CRA. Faster to start your next react project.", + "keywords": "Keywords", + "created_by": "Created with by", + "copy_right": "Copyright (c) 2021 by Aldenn" + }, + "vi": { + "title": "React-Typescript-Webpack được xây dựng từ React, Typescript và Webpack mà không sử dụng CRA. Giúp người dùng bắt đầu nhanh một dự án từ React", + "keywords": "Các từ khoá", + "created_by": "Được thiết kế bởi ", + "copy_right": "Bản quyền (c) 2021 bởi Aldenn" + } +} diff --git a/src/locales/resources/index.ts b/src/locales/resources/index.ts new file mode 100644 index 0000000..2d6457c --- /dev/null +++ b/src/locales/resources/index.ts @@ -0,0 +1,30 @@ +import { Resource } from 'i18next'; +import languages from '../languages'; +import home from './home.json'; +import footer from './footer.json'; + +interface IResource { + [key: string]: Resource; +} + +const mergeResource: IResource = { + home, + footer, +}; + +export const langs = Object.keys(languages); + +const resources: Resource = {}; + +Object.keys(languages).map(lang => { + Object.keys(mergeResource).map(fileName => { + resources[lang] = { + translation: { + ...(resources[lang]?.translation as object), + [fileName]: mergeResource[fileName][lang], + }, + }; + }); +}); + +export default resources; diff --git a/src/locales/vi/home.json b/src/locales/vi/home.json deleted file mode 100644 index cdba756..0000000 --- a/src/locales/vi/home.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "title": "React-Typescript-Webpack được xây dựng từ React, Typescript và Webpack mà không sử dụng CRA. Giúp người dùng bắt đầu nhanh một dự án từ React", - "keywords": "Các từ khoá", - "created_by": "Được thiết kế bởi ", - "copy_right": "Bản quyền (c) 2021 bởi Aldenn" -}