diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..397720bc --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +src/locales/*.json +node_modules/ +dist/ +build/ \ No newline at end of file diff --git a/package.json b/package.json index 591f31f8..96fa5455 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.js b/src/App.js index 3d9cb4f3..6004752c 100644 --- a/src/App.js +++ b/src/App.js @@ -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'; diff --git a/src/components/LanguageSwitch.js b/src/components/LanguageSwitch.js new file mode 100644 index 00000000..ab6a3af9 --- /dev/null +++ b/src/components/LanguageSwitch.js @@ -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 ( + + + {currentLanguage.text} + + } + options={languages} + pointing="top right" + icon={null} + value={i18n.language} + onChange={handleLanguageChange} + selectOnBlur={false} + /> + ); +} \ No newline at end of file diff --git a/src/components/SearchBar.js b/src/components/SearchBar.js index ec64d966..e13f7cdd 100644 --- a/src/components/SearchBar.js +++ b/src/components/SearchBar.js @@ -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 = [ @@ -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({ @@ -78,16 +80,16 @@ export default function SearchBar(props) { return (
- Search over GitHub, Bitbucket, GitLab to find real-world usage variable names + {t('searchBar.description')}
updateState({ valChanged: true })} className='search-bar__input' - icon fluid placeholder={props.placeholder} size={inputSize}> + icon fluid placeholder={t('searchBar.placeholder')} size={inputSize}> - + {langItems} @@ -106,7 +108,7 @@ export default function SearchBar(props) {
- Extensions:  + {t('searchBar.extensions')}  VS Code -
Nothing found, please try or come back later :)
-
You can also get help from https://github.com/unbug/codelf/issues. +
+ Nothing found, please try or come back later :) +
+
+ {t('search.helpText')}
); diff --git a/src/components/SourceCode.js b/src/components/SourceCode.js index 2f22ed11..456deba0 100644 --- a/src/components/SourceCode.js +++ b/src/components/SourceCode.js @@ -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() { @@ -14,15 +16,15 @@ export default function SourceCode(props) { if (!props.sourceCodeVariable || !props.sourceCodeRepo) { return null; } const sourceCodeVariable = props.sourceCodeVariable; const dropText = ( -
All Codes
diff --git a/src/i18n.js b/src/i18n.js new file mode 100644 index 00000000..23786c7b --- /dev/null +++ b/src/i18n.js @@ -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; \ No newline at end of file diff --git a/src/locales/de.json b/src/locales/de.json new file mode 100644 index 00000000..a1d3d551 --- /dev/null +++ b/src/locales/de.json @@ -0,0 +1,47 @@ +{ + "searchBar": { + "description": "Durchsuche GitHub, Bitbucket, GitLab um echte Verwendungen von Variablennamen zu finden", + "placeholder": "KI Künstliche Intelligenz", + "allLanguages": "Alle 90 Sprachen (Zurücksetzen)", + "extensions": "Erweiterungen:", + "filter": "Sprachen filtern" + }, + "search": { + "button": "Suchen", + "loading": "Suche läuft...", + "noResults": "Nichts gefunden, bitte versuche Schnellsuche oder komm später wieder :)", + "quickSearch": "Schnellsuche", + "helpText": "Du kannst auch Hilfe unter https://github.com/unbug/codelf/issues finden." + }, + "variable": { + "search": "Suchen", + "repo": "Repo", + "copy": "Kopieren", + "codes": "Code" + }, + "navbar": { + "starMe": "Markiere mich auf GitHub", + "bookmark": "一分钟读论文", + "language": "Sprache" + }, + "notices": { + "micropaper": "[Micropaper]一分钟读懂一篇论文", + "suicide": "SAG NEIN ZUR SUICIDE PUBLIC LICENSE", + "mihtool": "[MIHTool] iOS Tool zum Debuggen und Optimieren von Seiten", + "wasmRocks": "WebAssembly ist großartig", + "reactNative": "[Open Source] React Native Entwicklungsschulungsmaterialien und Videos", + "bytedance": "[Empfehlung]ByteDance China/USA/Singapur Stellenausschreibung/Campus-Rekrutierung/Praktikum" + }, + "donate": { + "title": "Spendier mir einen Kaffee", + "description": "Wenn dir dieses Projekt geholfen hat, ziehe bitte eine Spende in Betracht, um es zu unterstützen." + }, + "sourceCode": { + "title": "Quellcode", + "close": "Schließen", + "repository": "Repository" + }, + "suggestion": { + "title": "Vorschläge" + } +} \ No newline at end of file diff --git a/src/locales/en.json b/src/locales/en.json new file mode 100644 index 00000000..1503ea5c --- /dev/null +++ b/src/locales/en.json @@ -0,0 +1,47 @@ +{ + "searchBar": { + "description": "Search over GitHub, Bitbucket, GitLab to find real-world usage variable names", + "placeholder": "AI Artificial Intelligence", + "allLanguages": "All 90 Languages (Reset)", + "extensions": "Extensions:", + "filter": "Filter languages" + }, + "search": { + "button": "Search", + "loading": "Searching...", + "noResults": "Nothing found, please try Quick Search or come back later :)", + "quickSearch": "Quick Search", + "helpText": "You can also get help from https://github.com/unbug/codelf/issues." + }, + "variable": { + "search": "Search", + "repo": "Repo", + "copy": "Copy", + "codes": "Codes" + }, + "navbar": { + "starMe": "Star me on GitHub", + "bookmark": "一分钟读论文", + "language": "Language" + }, + "notices": { + "micropaper": "[Micropaper]一分钟读懂一篇论文", + "suicide": "SAY NO TO SUICIDE PUBLIC LICENSE", + "mihtool": "[MIHTool] iOS 上调试和优化页面的工具", + "wasmRocks": "WebAssembly Rocks", + "reactNative": "[开源] React Native 开发培训资料和视频", + "bytedance": "[内推]字节跳动中国/美国/新加坡社招/校招/实习" + }, + "donate": { + "title": "Buy me a coffee", + "description": "If this project helped you, please consider donating to support it." + }, + "sourceCode": { + "title": "Source Code", + "close": "Close", + "repository": "Repository" + }, + "suggestion": { + "title": "Suggestions" + } +} \ No newline at end of file diff --git a/src/locales/fr.json b/src/locales/fr.json new file mode 100644 index 00000000..79bfcbcc --- /dev/null +++ b/src/locales/fr.json @@ -0,0 +1,47 @@ +{ + "searchBar": { + "description": "Rechercher sur GitHub, Bitbucket, GitLab pour trouver des usages réels de noms de variables", + "placeholder": "IA Intelligence Artificielle", + "allLanguages": "Tous les 90 langages (Réinitialiser)", + "extensions": "Extensions:", + "filter": "Filtrer les langages" + }, + "search": { + "button": "Rechercher", + "loading": "Recherche en cours...", + "noResults": "Rien trouvé, veuillez essayer la recherche rapide ou revenir plus tard :)", + "quickSearch": "Recherche rapide", + "helpText": "Vous pouvez également obtenir de l'aide sur https://github.com/unbug/codelf/issues." + }, + "variable": { + "search": "Rechercher", + "repo": "Dépôt", + "copy": "Copier", + "codes": "Codes" + }, + "navbar": { + "starMe": "Étoile-moi sur GitHub", + "bookmark": "一分钟读论文", + "language": "Langue" + }, + "notices": { + "micropaper": "[Micropaper]一分钟读懂一篇论文", + "suicide": "DITES NON À LA SUICIDE PUBLIC LICENSE", + "mihtool": "[MIHTool] Outil iOS pour déboguer et optimiser les pages", + "wasmRocks": "WebAssembly est génial", + "reactNative": "[Open Source] Matériels de formation et vidéos pour le développement React Native", + "bytedance": "[Recommandation]ByteDance Chine/États-Unis/Singapour recrutement social/campus/stage" + }, + "donate": { + "title": "Offrez-moi un café", + "description": "Si ce projet vous a aidé, veuillez envisager de faire un don pour le soutenir." + }, + "sourceCode": { + "title": "Code source", + "close": "Fermer", + "repository": "Dépôt" + }, + "suggestion": { + "title": "Suggestions" + } +} \ No newline at end of file diff --git a/src/locales/zh.json b/src/locales/zh.json new file mode 100644 index 00000000..e2fa91c7 --- /dev/null +++ b/src/locales/zh.json @@ -0,0 +1,47 @@ +{ + "searchBar": { + "description": "搜索 GitHub、Bitbucket、GitLab 项目,寻找现实世界中变量名的使用", + "placeholder": "AI 人工智能", + "allLanguages": "全部 90 种语言(重置)", + "extensions": "扩展插件:", + "filter": "筛选语言" + }, + "search": { + "button": "搜索", + "loading": "搜索中...", + "noResults": "没有找到结果,请尝试快速搜索或稍后再试 :)", + "quickSearch": "快速搜索", + "helpText": "您也可以从 https://github.com/unbug/codelf/issues 获得帮助。" + }, + "variable": { + "search": "搜索", + "repo": "代码库", + "copy": "复制", + "codes": "代码" + }, + "navbar": { + "starMe": "在 GitHub 上为我加星", + "bookmark": "一分钟读论文", + "language": "语言" + }, + "notices": { + "micropaper": "[微论文] 一分钟读懂一篇论文", + "suicide": "拒绝自杀公共许可证", + "mihtool": "[MIHTool] iOS 上调试和优化页面的工具", + "wasmRocks": "WebAssembly 优秀", + "reactNative": "[开源] React Native 开发培训资料和视频", + "bytedance": "[内推] 字节跳动中国/美国/新加坡社招/校招/实习" + }, + "donate": { + "title": "请我喝杯咖啡", + "description": "如果这个项目对您有帮助,请考虑捐赠以支持它。" + }, + "sourceCode": { + "title": "源代码", + "close": "关闭", + "repository": "代码库" + }, + "suggestion": { + "title": "建议" + } +} \ No newline at end of file diff --git a/styles/_nav-bar-container.scss b/styles/_nav-bar-container.scss index e1d6fcc4..484e8e88 100755 --- a/styles/_nav-bar-container.scss +++ b/styles/_nav-bar-container.scss @@ -36,9 +36,22 @@ .copybook-btn { display: none; } + // Language switcher styles + .language-switch { + color: #70B7FD; + font-size: 0.9rem; + margin-top: 0.5rem; + &:hover { + color: #59AAF9; + } + i { + font-size: 1.2rem !important; + margin-right: 0.3rem !important; + } + } $anim-delay: 150ms; $anim-duration: 500ms; - @for $i from 1 through 5 { + @for $i from 1 through 6 { >*:nth-child(#{$i}) { animation-duration: $anim-duration; animation-delay: 500 + $anim-delay*($i - 1); @@ -62,6 +75,13 @@ font-size: 3.3rem; } } + .language-switch { + margin-top: 1rem; + font-size: 1rem; + i { + font-size: 1.5rem !important; + } + } } } }