Skip to content

Implement Chinese and English internationalization (i18n) support using i18next #144

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: codelf2023
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
src/locales/*.json
node_modules/
dist/
build/
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,14 @@
"css-doodle": "^0.7.6",
"events": "^3.0.0",
"file-saver": "^2.0.2",
"i18next": "^25.2.1",
"i18next-browser-languagedetector": "^8.1.0",
"lodash": "^4.17.15",
"mark.js": "^8.11.1",
"nprogress": "^0.2.0",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-i18next": "^15.5.2",
"react-router-dom": "^5.1.2",
"semantic-ui-css": "^2.4.1",
"semantic-ui-react": "^0.88.1",
Expand Down
1 change: 1 addition & 0 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'whatwg-fetch';
import ReactDOM from 'react-dom';
import './i18n'; // Initialize i18n
import MainContainer from './containers/MainContainer';
// import CopybookContainer from './containers/CopybookContainer';
import NoticeContainer from './containers/NoticeContainer';
Expand Down
37 changes: 37 additions & 0 deletions src/components/LanguageSwitch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react';
import { Dropdown, Icon } from 'semantic-ui-react';
import { useTranslation } from 'react-i18next';

const languages = [
{ key: 'en', text: 'English', value: 'en', flag: 'us' },
{ key: 'zh', text: '中文', value: 'zh', flag: 'cn' },
{ key: 'de', text: 'Deutsch', value: 'de', flag: 'de' },
{ key: 'fr', text: 'Français', value: 'fr', flag: 'fr' }
];

export default function LanguageSwitch() {
const { i18n } = useTranslation();

const handleLanguageChange = (e, { value }) => {
i18n.changeLanguage(value);
};

const currentLanguage = languages.find(lang => lang.value === i18n.language) || languages[0];

return (
<Dropdown
trigger={
<span className="language-switch">
<Icon name="world" />
{currentLanguage.text}
</span>
}
options={languages}
pointing="top right"
icon={null}
value={i18n.language}
onChange={handleLanguageChange}
selectOnBlur={false}
/>
);
}
10 changes: 6 additions & 4 deletions src/components/SearchBar.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useEffect, useRef, useState } from 'react';
import { Dropdown, Icon, Input } from 'semantic-ui-react';
import { useTranslation } from 'react-i18next';

// http://githut.info/
const topProgramLan = [
Expand Down Expand Up @@ -29,6 +30,7 @@ const topProgramLan = [
];

export default function SearchBar(props) {
const { t } = useTranslation();
const inputEl = useRef(null);
const inputSize = useInputSize('huge');
const [state, setState] = useState({
Expand Down Expand Up @@ -78,16 +80,16 @@ export default function SearchBar(props) {
return (
<div className='search-bar'>
<div className='search-bar__desc'>
Search over GitHub, Bitbucket, GitLab to find real-world usage variable names
{t('searchBar.description')}
</div>
<form action="javascript:void(0);">
<Input ref={inputEl}
onChange={() => updateState({ valChanged: true })}
className='search-bar__input'
icon fluid placeholder={props.placeholder} size={inputSize}>
icon fluid placeholder={t('searchBar.placeholder')} size={inputSize}>
<Dropdown floating text='' icon='filter' className='search-bar__dropdown'>
<Dropdown.Menu>
<Dropdown.Item icon='undo' text='All 90 Languages (Reset)' onClick={handleRestLang} />
<Dropdown.Item icon='undo' text={t('searchBar.allLanguages')} onClick={handleRestLang} />
<Dropdown.Menu scrolling className='fix-dropdown-menu'>
{langItems}
</Dropdown.Menu>
Expand All @@ -106,7 +108,7 @@ export default function SearchBar(props) {
</Input>
</form>
<div className='search-bar__plugins'>
Extensions:&nbsp;
{t('searchBar.extensions')}&nbsp;
<a href='https://github.com/unbug/codelf#codelf-for-vs-code'
target='_blank' rel='noopener noreferrer'>VS Code</a>,&nbsp;
<a className='text-muted' href='https://atom.io/packages/codelf'
Expand Down
12 changes: 8 additions & 4 deletions src/components/SearchError.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import React from 'react';
import { Label } from 'semantic-ui-react';
import { useTranslation } from 'react-i18next';

export default function SearchError() {
const { t } = useTranslation();

return (
<div className='search-error'>
<div>Nothing found, please try <Label color='grey' size='mini'>Quick Search</Label> or come back later :)</div>
<div>You can also get help from <a href='https://github.com/unbug/codelf/issues'
target='_blank'
rel="noopener noreferrer" >https://github.com/unbug/codelf/issues</a>.
<div>
Nothing found, please try <Label color='grey' size='mini'>{t('search.quickSearch')}</Label> or come back later :)
</div>
<div>
{t('search.helpText')}
</div>
</div>
);
Expand Down
10 changes: 6 additions & 4 deletions src/components/SourceCode.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React from 'react';
import { Modal, Button, Dropdown, Label } from 'semantic-ui-react';
import { useTranslation } from 'react-i18next';
import * as Tools from '../utils/Tools';
import Loading from "./Loading";
import useCodeHighlighting from './hooks/useCodeHighlighting';

export default function SourceCode(props) {
const { t } = useTranslation();
const codeEl = useCodeHighlighting([props.sourceCode, props.sourceCodeVisible], props.sourceCodeVariable ?.keyword);

function handleClose() {
Expand All @@ -14,15 +16,15 @@ export default function SourceCode(props) {
if (!props.sourceCodeVariable || !props.sourceCodeRepo) { return null; }
const sourceCodeVariable = props.sourceCodeVariable;
const dropText = (
<div>All Codes <Label size='mini' circular color={sourceCodeVariable.color}>
<div>{t('variable.codes')} <Label size='mini' circular color={sourceCodeVariable.color}>
{sourceCodeVariable.repoList.length}
</Label></div>
);
const dropdownItems = props.sourceCodeVariable && props.sourceCodeVariable.repoList.map(repo => {
return (
<Dropdown.Item key={Tools.uuid()}>
<Button size='mini' onClick={() => props.onRequestSourceCode(repo)}>Codes</Button>
<Button size='mini' as='a' href={repo.repo} target='_blank'>Repo</Button>
<Button size='mini' onClick={() => props.onRequestSourceCode(repo)}>{t('variable.codes')}</Button>
<Button size='mini' as='a' href={repo.repo} target='_blank'>{t('variable.repo')}</Button>
<Label size='mini' circular color={Tools.randomLabelColor()}>{repo.language}</Label>
</Dropdown.Item>
)
Expand All @@ -39,7 +41,7 @@ export default function SourceCode(props) {
</Dropdown.Menu>
</Dropdown.Menu>
</Dropdown>
<Button size='mini' as='a' href={props.sourceCodeRepo.repo} target='_blank'>Repo</Button>
<Button size='mini' as='a' href={props.sourceCodeRepo.repo} target='_blank'>{t('variable.repo')}</Button>
</Modal.Header>
<Modal.Content>
{props.sourceCodeRequesting ? <Loading /> : ''}
Expand Down
5 changes: 4 additions & 1 deletion src/components/Suggestion.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import React from 'react';
import { Label } from 'semantic-ui-react';
import { useTranslation } from 'react-i18next';

export default function Suggestion(props) {
const { t } = useTranslation();

if (!props.suggestion || !props.suggestion.length) { return null; }
const list = props.suggestion.map((item, i) => {
return <Label key={i} circular size='mini' color='grey' as='a' href={`#${item}`}>{item}</Label>
});
return (
<div className='suggestion'>
<Label color='grey' size='mini'>Quick Search:</Label> {list}
<Label color='grey' size='mini'>{t('search.quickSearch')}:</Label> {list}
</div>
)
}
11 changes: 7 additions & 4 deletions src/components/VariableItem.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import React from 'react';
import { Button, Label, Popup } from 'semantic-ui-react';
import { useTranslation } from 'react-i18next';
import * as Tools from '../utils/Tools';

export default function VariableItem(props) {
const { t } = useTranslation();
const clipboardId = `clipboardId-${Tools.uuid()}`;
const variable = props.variable;
let clipboard = null;

function handlePopOnMount() {
/* global ClipboardJS */
clipboard = new ClipboardJS(`#${clipboardId}`);
}

Expand All @@ -26,11 +29,11 @@ export default function VariableItem(props) {
onUnmount={handlePopUnmount}
hoverable={true}>
<Button.Group vertical basic style={{ border: 0 }}>
<Button compact as='a' href={`#${variable.keyword}`}>Search</Button>
<Button compact as='a' href={variable.repoLink} target='_blank'>Repo</Button>
<Button compact data-clipboard-text={variable.keyword} id={clipboardId}>Copy</Button>
<Button compact as='a' href={`#${variable.keyword}`}>{t('variable.search')}</Button>
<Button compact as='a' href={variable.repoLink} target='_blank'>{t('variable.repo')}</Button>
<Button compact data-clipboard-text={variable.keyword} id={clipboardId}>{t('variable.copy')}</Button>
<Button compact onClick={() => props.onOpenSourceCode(variable)}>
[{variable.repoLang}] Codes <Label size='mini' circular color={variable.color}>{variable.repoList.length}</Label>
[{variable.repoLang}] {t('variable.codes')} <Label size='mini' circular color={variable.color}>{variable.repoList.length}</Label>
</Button>
</Button.Group>
</Popup>
Expand Down
4 changes: 3 additions & 1 deletion src/containers/MainContainer.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useEffect, useReducer, useCallback } from 'react';
import { Container } from 'semantic-ui-react';
import { useTranslation } from 'react-i18next';
import SearchBar from '../components/SearchBar';
import TitleLogo from '../components/TitleLogo';
import SearchCodeModel from '../models/SearchCodeModel';
Expand Down Expand Up @@ -48,6 +49,7 @@ function reducer(state, action) {


export default function MainContainer(props) {
const { t } = useTranslation();
const [state, dispatch] = useReducer(reducer, initState);

useEffect(() => {
Expand Down Expand Up @@ -191,7 +193,7 @@ export default function MainContainer(props) {
return (
<Container className='main-container'>
<TitleLogo />
<SearchBar placeholder='AI 人工智能' {...state} onSearch={handleSearch} />
<SearchBar placeholder={t('searchBar.placeholder')} {...state} onSearch={handleSearch} />
<Suggestion {...state} />
{state.variableRequesting ? <Loading /> : (state.isError ? <SearchError /> : '')}
{renderSloganImage()}
Expand Down
13 changes: 7 additions & 6 deletions src/containers/NavBarContainer.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React from 'react';
import { Container, Icon, Popup } from 'semantic-ui-react';
import { Container, Icon } from 'semantic-ui-react';
import { useTranslation } from 'react-i18next';
import LanguageSwitch from '../components/LanguageSwitch';
// import CopybookModel from '../models/CopybookModel';

export default function NavBarContainer() {
function handleOpenCopybook() {
// CopybookModel.update({ visible: true });
}
const { t } = useTranslation();

return (
<Container className='nav-bar-container'>
Expand All @@ -29,13 +29,14 @@ export default function NavBarContainer() {
Sorry, GitHub stars organize tool currently is not available, <a href="https://github.com/unbug/codelf/projects/2" target='_blank' rel='noopener noreferrer'>new version</a> is coming soon :)
</Popup>
*/}
<LanguageSwitch />
<a href='https://unbug.github.io' className='bookmark-btn animated fadeInDown'
title='一分钟读论文'
title={t('navbar.bookmark')}
target='_blank' rel='noopener noreferrer'>
<Icon name='bookmark' />
</a>
<a href='https://github.com/unbug/codelf' className='github-corner animated fadeInDown'
title='Star me on GitHub'
title={t('navbar.starMe')}
target='_blank' rel='noopener noreferrer'>
<Icon name='github square' />
</a>
Expand Down
16 changes: 9 additions & 7 deletions src/containers/NoticeContainer.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,31 @@
import React, { useState, useEffect, useRef } from 'react';
import { Icon, Popup } from 'semantic-ui-react';
import { useTranslation } from 'react-i18next';

export default function NoticeContainer() {
const { t } = useTranslation();
const listEl = useRef(null);
const [activeIndex, setDisable] = useSliderEffect(listEl);
const [, setDisable] = useSliderEffect(listEl);

return (
<div className='notice-container' ref={listEl} onMouseEnter={() => setDisable(true)} onMouseLeave={() => setDisable(false)}>
<a className='animated fadeIn show' target='_blank' rel='noopener noreferrer'
href='https://unbug.github.io'>
<Icon name='newspaper' /> [Micropaper]一分钟读懂一篇论文
<Icon name='newspaper' /> {t('notices.micropaper')}
</a>
<a className='animated fadeIn' target='_blank' rel='noopener noreferrer'
href='https://github.com/unbug/snts'>
<Icon name='heartbeat' /> SAY NO TO SUICIDE PUBLIC LICENSE
<Icon name='heartbeat' /> {t('notices.suicide')}
</a>
<a className='animated fadeIn' target='_blank' rel='noopener noreferrer' href='//mihtool.com/'>
<Icon name='code' /> [MIHTool] iOS 上调试和优化页面的工具
<Icon name='code' /> {t('notices.mihtool')}
</a>
<a className='animated fadeIn' target='_blank' rel='noopener noreferrer' href='https://www.wasmrocks.com/'>
<Icon name='hand rock' /> WebAssembly Rocks
<Icon name='hand rock' /> {t('notices.wasmRocks')}
</a>
<a className='animated fadeIn' target='_blank' rel='noopener noreferrer'
href='https://github.com/unbug/react-native-train/blob/master/README.md'>
<Icon name='video' /> [开源] React Native 开发培训资料和视频
<Icon name='video' /> {t('notices.reactNative')}
</a>
<a className='animated fadeIn' target='_blank' rel='noopener noreferrer'
href='https://job.toutiao.com/s/gKn4Ea'>
Expand Down Expand Up @@ -55,7 +57,7 @@ export default function NoticeContainer() {
</div>
}
trigger={
<span><Icon name='send' />[内推]字节跳动中国/美国/新加坡社招/校招/实习</span>
<span><Icon name='send' />{t('notices.bytedance')}</span>
} />
</a>
</div>
Expand Down
64 changes: 64 additions & 0 deletions src/i18n.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';

import en from './locales/en.json';
import zh from './locales/zh.json';
import de from './locales/de.json';
import fr from './locales/fr.json';

i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'en',
lng: undefined, // Let language detector determine the language
debug: false,

// Language detection options
detection: {
order: ['localStorage', 'navigator', 'htmlTag'],
caches: ['localStorage'],
lookupLocalStorage: 'codelf-language',
convertDetectedLanguage: (lng) => {
// Convert Chinese variants to 'zh'
if (lng.startsWith('zh')) {
return 'zh';
}
// Convert English variants to 'en'
if (lng.startsWith('en')) {
return 'en';
}
// Convert German variants to 'de'
if (lng.startsWith('de')) {
return 'de';
}
// Convert French variants to 'fr'
if (lng.startsWith('fr')) {
return 'fr';
}
return lng;
}
},

interpolation: {
escapeValue: false,
},

resources: {
en: {
translation: en
},
zh: {
translation: zh
},
de: {
translation: de
},
fr: {
translation: fr
}
}
});

export default i18n;
Loading