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 (
{
@@ -77,7 +80,7 @@ export const RunButton: FC<{ onClickRun?: MouseEventHandler }> = observer(({onCl
await onClickRun?.(e);
await onClickMainButton();
}}>
- {mainButtonText[commonStore.modelStatus]}
+ {t(mainButtonText[commonStore.modelStatus])}
);
});
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 (
-
{
{config.name}
)}
- } 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}/>
- Config Name
+ {t('Config Name')}
{
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(() => {
}
/>
-
{
modelItem.isLocal && {modelItem.name}
)}
- } 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(() => {
CUDA
}/>
- {
@@ -217,7 +219,7 @@ export const Configs: FC = observer(() => {
fp32
}/>
- {
@@ -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.')}