mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-11 00:06:04 +00:00
Add i18n support with language selector on login page (#1410)
* feat: add i18n support with language selector on login page Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com> * feat: complete i18n implementation for all webui components Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com> * feat: complete all hardcoded text * feat: dynamic label i18n * fix: lint errors * fix: lint errors * delete sh fils * fix: edit model dialog title --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Junyan Qin <Chin> <rockchinq@gmail.com>
This commit is contained in:
committed by
GitHub
parent
91cd8cf380
commit
2bf94539bd
@@ -20,6 +20,7 @@ import { GithubIcon } from 'lucide-react';
|
||||
import { useState, useRef } from 'react';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
enum PluginInstallStatus {
|
||||
WAIT_INPUT = 'wait_input',
|
||||
@@ -28,6 +29,7 @@ enum PluginInstallStatus {
|
||||
}
|
||||
|
||||
export default function PluginConfigPage() {
|
||||
const { t } = useTranslation();
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [sortModalOpen, setSortModalOpen] = useState(false);
|
||||
const [pluginInstallStatus, setPluginInstallStatus] =
|
||||
@@ -61,7 +63,7 @@ export default function PluginConfigPage() {
|
||||
} else {
|
||||
// success
|
||||
if (!alreadySuccess) {
|
||||
toast.success('插件安装成功');
|
||||
toast.success(t('plugins.installSuccess'));
|
||||
alreadySuccess = true;
|
||||
}
|
||||
setGithubURL('');
|
||||
@@ -85,10 +87,10 @@ export default function PluginConfigPage() {
|
||||
<div className="flex flex-row justify-between items-center px-[0.8rem]">
|
||||
<TabsList className="shadow-md py-5 bg-[#f0f0f0]">
|
||||
<TabsTrigger value="installed" className="px-6 py-4 cursor-pointer">
|
||||
已安装
|
||||
{t('plugins.installed')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="market" className="px-6 py-4 cursor-pointer">
|
||||
插件市场
|
||||
{t('plugins.marketplace')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
@@ -100,7 +102,7 @@ export default function PluginConfigPage() {
|
||||
setSortModalOpen(true);
|
||||
}}
|
||||
>
|
||||
编排
|
||||
{t('plugins.arrange')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
@@ -112,7 +114,7 @@ export default function PluginConfigPage() {
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
安装
|
||||
{t('plugins.install')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -136,14 +138,14 @@ export default function PluginConfigPage() {
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-4">
|
||||
<GithubIcon className="size-6" />
|
||||
<span>从 GitHub 安装插件</span>
|
||||
<span>{t('plugins.installFromGithub')}</span>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
{pluginInstallStatus === PluginInstallStatus.WAIT_INPUT && (
|
||||
<div className="mt-4">
|
||||
<p className="mb-2">目前仅支持从 GitHub 安装</p>
|
||||
<p className="mb-2">{t('plugins.onlySupportGithub')}</p>
|
||||
<Input
|
||||
placeholder="请输入插件的Github链接"
|
||||
placeholder={t('plugins.enterGithubLink')}
|
||||
value={githubURL}
|
||||
onChange={(e) => setGithubURL(e.target.value)}
|
||||
className="mb-4"
|
||||
@@ -152,12 +154,12 @@ export default function PluginConfigPage() {
|
||||
)}
|
||||
{pluginInstallStatus === PluginInstallStatus.INSTALLING && (
|
||||
<div className="mt-4">
|
||||
<p className="mb-2">正在安装插件...</p>
|
||||
<p className="mb-2">{t('plugins.installing')}</p>
|
||||
</div>
|
||||
)}
|
||||
{pluginInstallStatus === PluginInstallStatus.ERROR && (
|
||||
<div className="mt-4">
|
||||
<p className="mb-2">插件安装失败:</p>
|
||||
<p className="mb-2">{t('plugins.installFailed')}</p>
|
||||
<p className="mb-2 text-red-500">{installError}</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -165,14 +167,16 @@ export default function PluginConfigPage() {
|
||||
{pluginInstallStatus === PluginInstallStatus.WAIT_INPUT && (
|
||||
<>
|
||||
<Button variant="outline" onClick={() => setModalOpen(false)}>
|
||||
取消
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleModalConfirm}>
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
<Button onClick={handleModalConfirm}>确认</Button>
|
||||
</>
|
||||
)}
|
||||
{pluginInstallStatus === PluginInstallStatus.ERROR && (
|
||||
<Button variant="default" onClick={() => setModalOpen(false)}>
|
||||
关闭
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { i18nObj } from '@/i18n/I18nProvider';
|
||||
|
||||
export interface PluginInstalledComponentRef {
|
||||
refreshPluginList: () => void;
|
||||
@@ -20,6 +22,7 @@ export interface PluginInstalledComponentRef {
|
||||
// eslint-disable-next-line react/display-name
|
||||
const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
(props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [pluginList, setPluginList] = useState<PluginCardVO[]>([]);
|
||||
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||
const [selectedPlugin, setSelectedPlugin] = useState<PluginCardVO | null>(
|
||||
@@ -41,7 +44,7 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
value.plugins.map((plugin) => {
|
||||
return new PluginCardVO({
|
||||
author: plugin.author,
|
||||
description: plugin.description.zh_CN,
|
||||
description: i18nObj(plugin.description),
|
||||
enabled: plugin.enabled,
|
||||
name: plugin.name,
|
||||
version: plugin.version,
|
||||
@@ -77,14 +80,14 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
>
|
||||
<path d="M7 5C7 2.79086 8.79086 1 11 1C13.2091 1 15 2.79086 15 5H20C20.5523 5 21 5.44772 21 6V10.1707C21 10.4953 20.8424 10.7997 20.5774 10.9872C20.3123 11.1746 19.9728 11.2217 19.6668 11.1135C19.4595 11.0403 19.2355 11 19 11C17.8954 11 17 11.8954 17 13C17 14.1046 17.8954 15 19 15C19.2355 15 19.4595 14.9597 19.6668 14.8865C19.9728 14.7783 20.3123 14.8254 20.5774 15.0128C20.8424 15.2003 21 15.5047 21 15.8293V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V6C3 5.44772 3.44772 5 4 5H7ZM11 3C9.89543 3 9 3.89543 9 5C9 5.23554 9.0403 5.45952 9.11355 5.66675C9.22172 5.97282 9.17461 6.31235 8.98718 6.57739C8.79974 6.84243 8.49532 7 8.17071 7H5V19H19V17C16.7909 17 15 15.2091 15 13C15 10.7909 16.7909 9 19 9V7H13.8293C13.5047 7 13.2003 6.84243 13.0128 6.57739C12.8254 6.31235 12.7783 5.97282 12.8865 5.66675C12.9597 5.45952 13 5.23555 13 5C13 3.89543 12.1046 3 11 3Z"></path>
|
||||
</svg>
|
||||
<div className="text-lg mb-2">暂未安装任何插件</div>
|
||||
<div className="text-lg mb-2">{t('plugins.noPluginInstalled')}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={`${styles.pluginListContainer}`}>
|
||||
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
||||
<DialogContent className="w-[700px] max-h-[80vh] p-0 flex flex-col">
|
||||
<DialogHeader className="px-6 pt-6 pb-2">
|
||||
<DialogTitle>插件配置</DialogTitle>
|
||||
<DialogTitle>{t('plugins.pluginConfig')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 overflow-y-auto px-6">
|
||||
{selectedPlugin && (
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import { toast } from 'sonner';
|
||||
import { i18nObj } from '@/i18n/I18nProvider';
|
||||
|
||||
enum PluginRemoveStatus {
|
||||
WAIT_INPUT = 'WAIT_INPUT',
|
||||
@@ -179,7 +180,7 @@ export default function PluginForm({
|
||||
<div className="space-y-2">
|
||||
<div className="text-lg font-medium">{pluginInfo.name}</div>
|
||||
<div className="text-sm text-gray-500 pb-2">
|
||||
{pluginInfo.description.zh_CN}
|
||||
{i18nObj(pluginInfo.description)}
|
||||
</div>
|
||||
{pluginInfo.config_schema.length > 0 && (
|
||||
<DynamicFormComponent
|
||||
|
||||
@@ -5,6 +5,7 @@ import styles from '@/app/home/plugins/plugins.module.css';
|
||||
import { PluginMarketCardVO } from '@/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardVO';
|
||||
import PluginMarketCardComponent from '@/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardComponent';
|
||||
import { spaceClient } from '@/app/infra/http/HttpClient';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Pagination,
|
||||
@@ -27,6 +28,7 @@ export default function PluginMarketComponent({
|
||||
}: {
|
||||
askInstallPlugin: (githubURL: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [marketPluginList, setMarketPluginList] = useState<
|
||||
PluginMarketCardVO[]
|
||||
>([]);
|
||||
@@ -105,7 +107,7 @@ export default function PluginMarketComponent({
|
||||
console.log('market plugins:', res);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('获取插件列表失败:', error);
|
||||
console.error(t('plugins.getPluginListError'), error);
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
@@ -131,7 +133,7 @@ export default function PluginMarketComponent({
|
||||
width: '300px',
|
||||
}}
|
||||
value={searchKeyword}
|
||||
placeholder="搜索插件"
|
||||
placeholder={t('plugins.searchPlugin')}
|
||||
onChange={(e) => onInputSearchKeyword(e.target.value)}
|
||||
/>
|
||||
|
||||
@@ -140,12 +142,16 @@ export default function PluginMarketComponent({
|
||||
onValueChange={handleSortChange}
|
||||
>
|
||||
<SelectTrigger className="w-[180px] ml-2 cursor-pointer">
|
||||
<SelectValue placeholder="排序方式" />
|
||||
<SelectValue placeholder={t('plugins.sortBy')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="stars,DESC">最多星标</SelectItem>
|
||||
<SelectItem value="created_at,DESC">最近新增</SelectItem>
|
||||
<SelectItem value="pushed_at,DESC">最近更新</SelectItem>
|
||||
<SelectItem value="stars,DESC">{t('plugins.mostStars')}</SelectItem>
|
||||
<SelectItem value="created_at,DESC">
|
||||
{t('plugins.recentlyAdded')}
|
||||
</SelectItem>
|
||||
<SelectItem value="pushed_at,DESC">
|
||||
{t('plugins.recentlyUpdated')}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
@@ -221,11 +227,11 @@ export default function PluginMarketComponent({
|
||||
<div className={`${styles.pluginListContainer}`}>
|
||||
{loading ? (
|
||||
<div style={{ textAlign: 'center', padding: '20px' }}>
|
||||
{/* 加载中... */}
|
||||
{t('plugins.loading')}
|
||||
</div>
|
||||
) : marketPluginList.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '20px' }}>
|
||||
{/* 没有找到匹配的插件 */}
|
||||
{t('plugins.noMatchingPlugins')}
|
||||
</div>
|
||||
) : (
|
||||
marketPluginList.map((vo, index) => (
|
||||
|
||||
@@ -31,6 +31,8 @@ import {
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { i18nObj } from '@/i18n/I18nProvider';
|
||||
|
||||
interface PluginSortDialogProps {
|
||||
open: boolean;
|
||||
@@ -75,6 +77,7 @@ export default function PluginSortDialog({
|
||||
onOpenChange,
|
||||
onSortComplete,
|
||||
}: PluginSortDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [sortedPlugins, setSortedPlugins] = useState<PluginCardVO[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
@@ -84,7 +87,7 @@ export default function PluginSortDialog({
|
||||
value.plugins.map((plugin) => {
|
||||
return new PluginCardVO({
|
||||
author: plugin.author,
|
||||
description: plugin.description.zh_CN,
|
||||
description: i18nObj(plugin.description),
|
||||
enabled: plugin.enabled,
|
||||
name: plugin.name,
|
||||
version: plugin.version,
|
||||
@@ -146,12 +149,12 @@ export default function PluginSortDialog({
|
||||
httpClient
|
||||
.reorderPlugins(reorderElements)
|
||||
.then(() => {
|
||||
toast.success('插件排序成功');
|
||||
toast.success(t('plugins.pluginSortSuccess'));
|
||||
onSortComplete();
|
||||
onOpenChange(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error('排序失败:' + err.message);
|
||||
toast.error(t('plugins.pluginSortError') + err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
@@ -162,11 +165,11 @@ export default function PluginSortDialog({
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="w-[700px] max-h-[80vh] p-0 flex flex-col">
|
||||
<DialogHeader className="px-6 pt-6 pb-0">
|
||||
<DialogTitle>插件排序</DialogTitle>
|
||||
<DialogTitle>{t('plugins.pluginSort')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 overflow-y-auto px-6 py-0">
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
插件顺序会影响同一事件内的处理顺序,请拖动插件卡片排序
|
||||
{t('plugins.pluginSortDescription')}
|
||||
</p>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
@@ -194,10 +197,10 @@ export default function PluginSortDialog({
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
取消
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={isLoading}>
|
||||
{isLoading ? '保存中...' : '保存'}
|
||||
{isLoading ? t('common.saving') : t('common.save')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
Reference in New Issue
Block a user