diff --git a/frontend/i18nally.json b/frontend/i18nally.json new file mode 100644 index 0000000..6d6a83f --- /dev/null +++ b/frontend/i18nally.json @@ -0,0 +1,110 @@ +{ + "version": "1.2", + "profiles": [ + { + "id": "e4cca065-e0f7-49ce-a400-75dfba1270eb", + "name": "Default", + "keyNamingPattern": "NATURAL_LANGUAGE_PATTERN", + "sink": { + "id": "93a27d2a-ff09-4608-ba13-0c08bc34c2dc", + "type": "singleFile", + "pathToFile": "$PROJECT_DIR$/frontend/src/_locales/zh-hans/main.json", + "fileType": "json", + "nestingType": "DISABLED", + "placeholderFormatterName": "I18NEXT" + }, + "sources": [ + { + "id": "efa45b89-aed3-4bc2-97d0-fa6147d13f8c", + "type": "jsx", + "scopeName": "Project Files", + "scopePattern": "", + "defaultReplacementTemplate": "t(\"%key%\")", + "attributeReplacementTemplate": "", + "inlineTagsReplacementTemplate": "", + "recognizedReplacementTemplates": [], + "inlineTagNames": [ + "small", + "tt", + "big", + "sub", + "img", + "strong", + "code", + "data", + "samp", + "wbr", + "del", + "Page", + "slot", + "sup", + "br", + "output", + "abbr", + "a", + "b", + "acronym", + "bdi", + "meter", + "var", + "em", + "i", + "label", + "bdo", + "kbd", + "dfn", + "ins", + "ruby", + "input", + "q", + "s", + "u", + "cite", + "progress", + "time", + "mark", + "span" + ], + "translatableAttributeNames": [ + "alt", + "placeholder", + "label", + "title", + "aria-label", + "desc", + "text" + ], + "skipDefaultNamespace": false, + "inlineTagHandler": "NONE" + } + ] + } + ], + "ignores": { + "valuesInProject": [ + "use strict" + ], + "valuesInFile": {}, + "filesInProject": [], + "unignoredFunctionNames": [], + "unignoredFunctionArguments": {}, + "ignoredArrayKeys": [ + "template", + "date", + "dateFormat", + "el", + "query", + "type", + "sql", + "layout", + "component", + "condition", + "name", + "selector", + "id", + "class", + "key", + "middleware" + ] + } +} \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3d8a603..8ed7363 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -31,8 +31,10 @@ import {useMediaQuery} from 'usehooks-ts'; import {ToastContainer} from 'react-toastify'; import commonStore from './stores/commonStore'; import {observer} from 'mobx-react-lite'; +import {useTranslation} from 'react-i18next'; const App: FC = observer(() => { + const {t} = useTranslation(); const navigate = useNavigate(); const location = useLocation(); const mq = useMediaQuery('(min-width: 640px)'); @@ -57,7 +59,7 @@ const App: FC = observer(() => { > {pages.filter(page => page.top).map(({label, path, icon}, index) => ( - {mq && label} + {mq && t(label)} ))} @@ -70,7 +72,7 @@ const App: FC = observer(() => { > {pages.filter(page => !page.top).map(({label, path, icon}, index) => ( - {mq && label} + {mq && t(label)} ))} diff --git a/frontend/src/_locales/zh-hans/main.json b/frontend/src/_locales/zh-hans/main.json index 74bb69d..0348a1c 100644 --- a/frontend/src/_locales/zh-hans/main.json +++ b/frontend/src/_locales/zh-hans/main.json @@ -1,3 +1,55 @@ { - "Settings": "设置" + "Home": "主页", + "Train": "训练", + "About": "关于", + "Settings": "设置", + "Go to chat page": "前往聊天页", + "Manage your configs": "管理你的配置", + "Manage models": "管理模型", + "Run": "运行", + "Starting": "启动中", + "Loading": "读取模型中", + "Stop": "停止", + "Enable High Precision For Last Layer": "输出层使用高精度", + "Stored Layers": "载入显存层数", + "Precision": "精度", + "Device": "设备", + "Convert model with these configs": "用这些设置转换模型", + "Manage Models": "管理模型", + "Model": "模型", + "Model Parameters": "模型参数", + "Frequency Penalty *": "Frequency Penalty *", + "Presence Penalty *": "Presence Penalty *", + "Top_P *": "Top_P *", + "Temperature *": "Temperature *", + "Max Response Token *": "最大响应 Token *", + "API Port": "API 端口", + "Hover your mouse over the text to view a detailed description. Settings marked with * will take effect immediately after being saved.": "把鼠标悬停在文本上查看详细描述. 标记了星号 * 的设置在保存后会立即生效.", + "Default API Parameters": "默认 API 参数", + "Provide JSON file URLs for the models manifest. Separate URLs with semicolons. The \"models\" field in JSON files will be parsed into the following table.": "填写模型描述的 JSON 文件地址. 地址间用分号分隔. JSON 文件内的 \"models\" 字段会被分析进下表.", + "Config Name": "配置名", + "Refresh": "刷新", + "Save Config": "保存配置", + "Model Source Manifest List": "模型源", + "Models": "模型", + "Delete Config": "删除配置", + "Help": "帮助", + "Version": "版本", + "New Config": "新建配置", + "Open Url": "打开网页", + "Download": "下载", + "Open Folder": "打开文件夹", + "Configs": "配置", + "Automatic Updates Check": "自动检查更新", + "Introduction": "介绍", + "Dark Mode": "深色模式", + "Language": "语言", + "In Development": "开发中", + "Chat": "聊天", + "Convert": "转换", + "Actions": "动作", + "Last updated": "上次更新", + "Desc": "描述", + "Size": "文件大小", + "File": "文件" } \ No newline at end of file diff --git a/frontend/src/components/RunButton.tsx b/frontend/src/components/RunButton.tsx index 415e7b0..7b890e1 100644 --- a/frontend/src/components/RunButton.tsx +++ b/frontend/src/components/RunButton.tsx @@ -7,6 +7,7 @@ import {exit, readRoot, switchModel, updateConfig} from '../apis'; import {toast} from 'react-toastify'; import manifest from '../../../manifest.json'; import {getStrategy} from '../utils'; +import {useTranslation} from 'react-i18next'; const mainButtonText = { [ModelStatus.Offline]: 'Run', @@ -70,6 +71,8 @@ const onClickMainButton = async () => { }; export const RunButton: FC<{ onClickRun?: MouseEventHandler }> = observer(({onClickRun}) => { + const {t} = useTranslation(); + return ( ); }); diff --git a/frontend/src/components/Section.tsx b/frontend/src/components/Section.tsx index 05e350f..ce65058 100644 --- a/frontend/src/components/Section.tsx +++ b/frontend/src/components/Section.tsx @@ -2,7 +2,7 @@ import {FC, ReactElement} from 'react'; import {Card, Text} from '@fluentui/react-components'; export const Section: FC<{ - title: string; desc?: string, content: ReactElement, outline?: boolean + title: string; desc?: string | null, content: ReactElement, outline?: boolean }> = ({title, desc, content, outline = true}) => { return ( diff --git a/frontend/src/components/ToolTipButton.tsx b/frontend/src/components/ToolTipButton.tsx index ab0c5fd..993ed46 100644 --- a/frontend/src/components/ToolTipButton.tsx +++ b/frontend/src/components/ToolTipButton.tsx @@ -2,7 +2,7 @@ import React, {FC, MouseEventHandler, ReactElement} from 'react'; import {Button, Tooltip} from '@fluentui/react-components'; export const ToolTipButton: FC<{ - text?: string, desc: string, icon?: ReactElement, onClick?: MouseEventHandler + text?: string | null, desc: string, icon?: ReactElement, onClick?: MouseEventHandler }> = ({ text, desc, diff --git a/frontend/src/pages/About.tsx b/frontend/src/pages/About.tsx index adf2eae..df5683d 100644 --- a/frontend/src/pages/About.tsx +++ b/frontend/src/pages/About.tsx @@ -1,10 +1,13 @@ import React, {FC} from 'react'; import {Text} from '@fluentui/react-components'; +import {useTranslation} from 'react-i18next'; export const About: FC = () => { + const {t} = useTranslation(); + return (
- In Development + {t('In Development')}
); }; diff --git a/frontend/src/pages/Chat.tsx b/frontend/src/pages/Chat.tsx index 4fec0c7..e64caa3 100644 --- a/frontend/src/pages/Chat.tsx +++ b/frontend/src/pages/Chat.tsx @@ -1,10 +1,13 @@ import React, {FC} from 'react'; import {Page} from '../components/Page'; import {PresenceBadge} from '@fluentui/react-components'; +import {useTranslation} from 'react-i18next'; export const Chat: FC = () => { + const {t} = useTranslation(); + return ( - diff --git a/frontend/src/pages/Configs.tsx b/frontend/src/pages/Configs.tsx index b5a6591..6d00f4f 100644 --- a/frontend/src/pages/Configs.tsx +++ b/frontend/src/pages/Configs.tsx @@ -16,8 +16,10 @@ import {updateConfig} from '../apis'; import {ConvertModel} from '../../wailsjs/go/backend_golang/App'; import manifest from '../../../manifest.json'; import {getStrategy, refreshLocalModels} from '../utils'; +import {useTranslation} from 'react-i18next'; export const Configs: FC = observer(() => { + const {t} = useTranslation(); const [selectedIndex, setSelectedIndex] = React.useState(commonStore.currentModelConfigIndex); const [selectedConfig, setSelectedConfig] = React.useState(commonStore.modelConfigs[selectedIndex]); @@ -66,7 +68,7 @@ export const Configs: FC = observer(() => { }; return ( -
{ )} - } onClick={() => { + } onClick={() => { commonStore.createModelConfig(); updateSelectedIndex(commonStore.modelConfigs.length - 1); }}/> - } onClick={() => { + } onClick={() => { commonStore.deleteModelConfig(selectedIndex); updateSelectedIndex(Math.min(selectedIndex, commonStore.modelConfigs.length - 1)); }}/> - } onClick={onClickSave}/> + } onClick={onClickSave}/>
- + { setSelectedConfigName(data.value); }}/>
- { setSelectedConfigApiParams({ @@ -110,7 +112,7 @@ export const Configs: FC = observer(() => { }); }}/> }/> - { @@ -119,7 +121,7 @@ export const Configs: FC = observer(() => { }); }}/> }/> - { setSelectedConfigApiParams({ @@ -127,7 +129,7 @@ export const Configs: FC = observer(() => { }); }}/> }/> - { setSelectedConfigApiParams({ @@ -135,7 +137,7 @@ export const Configs: FC = observer(() => { }); }}/> }/> - { setSelectedConfigApiParams({ @@ -143,7 +145,7 @@ export const Configs: FC = observer(() => { }); }}/> }/> - { setSelectedConfigApiParams({ @@ -155,10 +157,10 @@ export const Configs: FC = observer(() => { } />
- - } onClick={() => { + } onClick={() => { navigate({pathname: '/models'}); }}/>
}/> - { + { const modelPath = `${manifest.localModelDir}/${selectedConfig.modelParameters.modelName}`; const strategy = getStrategy(selectedConfig); const newModelPath = modelPath + '-' + strategy.replace(/[> *+]/g, '-'); @@ -188,7 +190,7 @@ export const Configs: FC = observer(() => { toast(`Convert Failed - ${e}`, {type: 'error'}); }); }}/> - { @@ -202,7 +204,7 @@ export const Configs: FC = observer(() => { }/> - { @@ -217,7 +219,7 @@ export const Configs: FC = observer(() => { }/> - { @@ -226,7 +228,7 @@ export const Configs: FC = observer(() => { }); }}/> }/> - { setSelectedConfigModelParams({ diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 8b5ce8d..f9f0489 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -13,6 +13,7 @@ import {observer} from 'mobx-react-lite'; import {RunButton} from '../components/RunButton'; import manifest from '../../../manifest.json'; import {BrowserOpenURL} from '../../wailsjs/runtime'; +import {useTranslation} from 'react-i18next'; type NavCard = { label: string; @@ -49,6 +50,7 @@ const navCards: NavCard[] = [ ]; export const Home: FC = observer(() => { + const {t} = useTranslation(); const navigate = useNavigate(); const onClickNavCard = (path: string) => { @@ -59,22 +61,17 @@ export const Home: FC = observer(() => {
- Introduction + {t('Introduction')}
- RWKV is an RNN with Transformer-level LLM performance, which can also be directly trained like a GPT - transformer (parallelizable). And it's 100% attention-free. You only need the hidden state at position t to - compute the state at position t+1. You can use the "GPT" mode to quickly compute the hidden state for the - "RNN" mode. -
- So it's combining the best of RNN and transformer - great performance, fast inference, saves VRAM, fast - training, "infinite" ctx_len, and free sentence embedding (using the final hidden state). + {t('RWKV is an RNN with Transformer-level LLM performance, which can also be directly trained like a GPT transformer (parallelizable). And it\'s 100% attention-free. You only need the hidden state at position t to compute the state at position t+1. You can use the "GPT" mode to quickly compute the hidden state for the "RNN" mode.
So it\'s combining the best of RNN and transformer - great performance, fast inference, saves VRAM, fast training, "infinite" ctx_len, and free sentence embedding (using the final hidden state).')} + {/*TODO Markdown*/}
{navCards.map(({label, path, icon, desc}, index) => ( - onClickNavCard(path)}> - {label} + {t(label)} ))}
@@ -96,8 +93,8 @@ export const Home: FC = observer(() => {
- Version: {manifest.version} - BrowserOpenURL('https://github.com/josStorer/RWKV-Runner')}>Help + {t('Version')}: {manifest.version} + BrowserOpenURL('https://github.com/josStorer/RWKV-Runner')}>{t('Help')}
diff --git a/frontend/src/pages/Models.tsx b/frontend/src/pages/Models.tsx index 1c0cc5d..d2ef0b4 100644 --- a/frontend/src/pages/Models.tsx +++ b/frontend/src/pages/Models.tsx @@ -22,6 +22,7 @@ import manifest from '../../../manifest.json'; import {toast} from 'react-toastify'; import {Page} from '../components/Page'; import {refreshModels, saveConfigs} from '../utils'; +import {useTranslation} from 'react-i18next'; const columns: TableColumnDefinition[] = [ createTableColumn({ @@ -30,7 +31,9 @@ const columns: TableColumnDefinition[] = [ return a.name.localeCompare(b.name); }, renderHeaderCell: () => { - return 'File'; + const {t} = useTranslation(); + + return t('File'); }, renderCell: (item) => { return ( @@ -49,12 +52,18 @@ const columns: TableColumnDefinition[] = [ return 0; }, renderHeaderCell: () => { - return 'Desc'; + const {t} = useTranslation(); + + return t('Desc'); }, renderCell: (item) => { + const lang: string = commonStore.settings.language; + return ( - {item.desc && item.desc['en']} + {item.desc && + (lang in item.desc ? item.desc[lang] : + ('en' in item.desc && item.desc['en']))} ); } @@ -65,7 +74,9 @@ const columns: TableColumnDefinition[] = [ return a.size - b.size; }, renderHeaderCell: () => { - return 'Size'; + const {t} = useTranslation(); + + return t('Size'); }, renderCell: (item) => { return ( @@ -85,7 +96,9 @@ const columns: TableColumnDefinition[] = [ return b.lastUpdatedMs - a.lastUpdatedMs; }, renderHeaderCell: () => { - return 'Last updated'; + const {t} = useTranslation(); + + return t('Last updated'); }, renderCell: (item) => { @@ -98,24 +111,28 @@ const columns: TableColumnDefinition[] = [ return a.isDownloading ? 0 : a.isLocal ? -1 : 1; }, renderHeaderCell: () => { - return 'Actions'; + const {t} = useTranslation(); + + return t('Actions'); }, renderCell: (item) => { + const {t} = useTranslation(); + return (
{ item.isLocal && - } onClick={() => { + } onClick={() => { OpenFileFolder(`./${manifest.localModelDir}/${item.name}`); }}/> } {item.downloadUrl && !item.isLocal && - } onClick={() => { + } onClick={() => { toast(`Downloading ${item.name}`); DownloadFile(`./${manifest.localModelDir}/${item.name}`, item.downloadUrl!); }}/>} - {item.url && } onClick={() => { + {item.url && } onClick={() => { BrowserOpenURL(item.url!); }}/>}
@@ -126,20 +143,21 @@ const columns: TableColumnDefinition[] = [ ]; export const Models: FC = observer(() => { + const {t} = useTranslation(); + return ( -
- Model Source Manifest List - } onClick={() => { + {t('Model Source Manifest List')} + } onClick={() => { refreshModels(false); saveConfigs(); }}/>
- Provide JSON file URLs for the models manifest. Separate URLs with semicolons. The "models" - field in JSON files will be parsed into the following table. + {t('Provide JSON file URLs for the models manifest. Separate URLs with semicolons. The "models" field in JSON files will be parsed into the following table.')}