mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-05 05:16:03 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
101e04db6d | ||
|
|
b79edda3a7 | ||
|
|
a20d3d11e5 | ||
|
|
3b4c455813 | ||
|
|
c967a2aa82 | ||
|
|
79cc6da96f |
@@ -240,12 +240,13 @@ class RuntimeMCPSession:
|
|||||||
return
|
return
|
||||||
if attempt >= self._MAX_RETRIES:
|
if attempt >= self._MAX_RETRIES:
|
||||||
self.status = MCPSessionStatus.ERROR
|
self.status = MCPSessionStatus.ERROR
|
||||||
self.error_message = f'Failed after {self._MAX_RETRIES + 1} attempts: {e}'
|
self.error_message = f'Failed after {self._MAX_RETRIES + 1} attempts: {self._describe_exception(e)}'
|
||||||
self._ready_event.set()
|
self._ready_event.set()
|
||||||
return
|
return
|
||||||
delay = self._RETRY_DELAYS[attempt]
|
delay = self._RETRY_DELAYS[attempt]
|
||||||
self.ap.logger.warning(
|
self.ap.logger.warning(
|
||||||
f'MCP session {self.server_name} failed (attempt {attempt + 1}), retrying in {delay}s: {e}'
|
f'MCP session {self.server_name} failed (attempt {attempt + 1}), '
|
||||||
|
f'retrying in {delay}s: {self._describe_exception(e)}'
|
||||||
)
|
)
|
||||||
await self._cleanup_box_stdio_session()
|
await self._cleanup_box_stdio_session()
|
||||||
# Reset status for retry
|
# Reset status for retry
|
||||||
@@ -254,6 +255,30 @@ class RuntimeMCPSession:
|
|||||||
self.error_phase = None
|
self.error_phase = None
|
||||||
await asyncio.sleep(delay)
|
await asyncio.sleep(delay)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _describe_exception(exc: BaseException) -> str:
|
||||||
|
"""Flatten an exception into its underlying leaf messages.
|
||||||
|
|
||||||
|
anyio / the MCP client wrap real failures in a TaskGroup, whose own
|
||||||
|
message is the unhelpful "unhandled errors in a TaskGroup (N
|
||||||
|
sub-exception)". Recurse into ExceptionGroups so the actual cause
|
||||||
|
(e.g. ``httpx.HTTPStatusError: Client error '410 Gone'``) is surfaced.
|
||||||
|
"""
|
||||||
|
leaves: list[str] = []
|
||||||
|
|
||||||
|
def visit(e: BaseException) -> None:
|
||||||
|
sub = getattr(e, 'exceptions', None)
|
||||||
|
if sub: # ExceptionGroup / BaseExceptionGroup
|
||||||
|
for child in sub:
|
||||||
|
visit(child)
|
||||||
|
else:
|
||||||
|
leaves.append(f'{type(e).__name__}: {e}')
|
||||||
|
|
||||||
|
visit(exc)
|
||||||
|
seen: set[str] = set()
|
||||||
|
unique = [m for m in leaves if not (m in seen or seen.add(m))]
|
||||||
|
return '; '.join(unique) if unique else f'{type(exc).__name__}: {exc}'
|
||||||
|
|
||||||
_MONITOR_POLL_INTERVAL = 5
|
_MONITOR_POLL_INTERVAL = 5
|
||||||
_MONITOR_MAX_CONSECUTIVE_ERRORS = 3
|
_MONITOR_MAX_CONSECUTIVE_ERRORS = 3
|
||||||
|
|
||||||
|
|||||||
@@ -119,6 +119,22 @@ function compareVersions(v1: string, v2: string): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Discord brand glyph (lucide-react has no Discord icon).
|
||||||
|
function DiscordIcon({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// IDs of sidebar entries that have collapsible entity sub-items
|
// IDs of sidebar entries that have collapsible entity sub-items
|
||||||
const ENTITY_CATEGORY_IDS = [
|
const ENTITY_CATEGORY_IDS = [
|
||||||
'bots',
|
'bots',
|
||||||
@@ -2077,6 +2093,14 @@ export default function HomeSidebar({
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
window.open('https://discord.gg/wdNEHETs87', '_blank');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DiscordIcon className="text-[#5865F2]" />
|
||||||
|
{t('common.joinDiscord')}
|
||||||
|
</DropdownMenuItem>
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
Download,
|
Download,
|
||||||
Package,
|
Package,
|
||||||
Server,
|
Server,
|
||||||
BookOpen,
|
Sparkles,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
XCircle,
|
XCircle,
|
||||||
Loader2,
|
Loader2,
|
||||||
@@ -176,7 +176,7 @@ function TaskProgressContent({ task }: { task: PluginInstallTask }) {
|
|||||||
// MCP / Skill don't have the plugin's download + dependency-install stages;
|
// MCP / Skill don't have the plugin's download + dependency-install stages;
|
||||||
// show a single "installing → done/failed" row instead of plugin steps.
|
// show a single "installing → done/failed" row instead of plugin steps.
|
||||||
const isPlugin = task.extensionType === 'plugin';
|
const isPlugin = task.extensionType === 'plugin';
|
||||||
const simpleIcon = task.extensionType === 'mcp' ? Server : BookOpen;
|
const simpleIcon = task.extensionType === 'mcp' ? Server : Sparkles;
|
||||||
const simpleInstallingLabel =
|
const simpleInstallingLabel =
|
||||||
task.extensionType === 'mcp'
|
task.extensionType === 'mcp'
|
||||||
? t('addExtension.installStage.mcpInstalling')
|
? t('addExtension.installStage.mcpInstalling')
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
X,
|
X,
|
||||||
ListTodo,
|
ListTodo,
|
||||||
Wrench,
|
Puzzle,
|
||||||
AudioWaveform,
|
Server,
|
||||||
Book,
|
Sparkles,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
@@ -35,9 +35,9 @@ const STAGE_ICONS: Record<string, React.ElementType> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const EXTENSION_TYPE_ICONS: Record<string, React.ElementType> = {
|
const EXTENSION_TYPE_ICONS: Record<string, React.ElementType> = {
|
||||||
plugin: Wrench,
|
plugin: Puzzle,
|
||||||
mcp: AudioWaveform,
|
mcp: Server,
|
||||||
skill: Book,
|
skill: Sparkles,
|
||||||
};
|
};
|
||||||
|
|
||||||
function TaskQueueItem({
|
function TaskQueueItem({
|
||||||
@@ -54,7 +54,7 @@ function TaskQueueItem({
|
|||||||
const isError = task.stage === InstallStage.ERROR;
|
const isError = task.stage === InstallStage.ERROR;
|
||||||
const isRunning = !isDone && !isError;
|
const isRunning = !isDone && !isError;
|
||||||
const StageIcon = STAGE_ICONS[task.stage] || Download;
|
const StageIcon = STAGE_ICONS[task.stage] || Download;
|
||||||
const TypeIcon = EXTENSION_TYPE_ICONS[task.extensionType] || Wrench;
|
const TypeIcon = EXTENSION_TYPE_ICONS[task.extensionType] || Puzzle;
|
||||||
|
|
||||||
const getTypeBadgeClass = () => {
|
const getTypeBadgeClass = () => {
|
||||||
switch (task.extensionType) {
|
switch (task.extensionType) {
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ import { extractI18nObject } from '@/i18n/I18nProvider';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useAsyncTask, AsyncTaskStatus } from '@/hooks/useAsyncTask';
|
import { useAsyncTask, AsyncTaskStatus } from '@/hooks/useAsyncTask';
|
||||||
import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext';
|
import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext';
|
||||||
import { Loader2, Puzzle } from 'lucide-react';
|
import { Loader2, Puzzle, Server, Sparkles } from 'lucide-react';
|
||||||
import { Wrench, AudioWaveform, Book } from 'lucide-react';
|
|
||||||
|
|
||||||
export interface PluginInstalledComponentRef {
|
export interface PluginInstalledComponentRef {
|
||||||
refreshPluginList: () => void;
|
refreshPluginList: () => void;
|
||||||
@@ -44,14 +43,18 @@ export const FilterOptions = [
|
|||||||
{
|
{
|
||||||
value: 'plugin' as FilterType,
|
value: 'plugin' as FilterType,
|
||||||
labelKey: 'market.typePlugin',
|
labelKey: 'market.typePlugin',
|
||||||
icon: Wrench,
|
icon: Puzzle,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'mcp' as FilterType,
|
value: 'mcp' as FilterType,
|
||||||
labelKey: 'market.typeMCP',
|
labelKey: 'market.typeMCP',
|
||||||
icon: AudioWaveform,
|
icon: Server,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'skill' as FilterType,
|
||||||
|
labelKey: 'market.typeSkill',
|
||||||
|
icon: Sparkles,
|
||||||
},
|
},
|
||||||
{ value: 'skill' as FilterType, labelKey: 'market.typeSkill', icon: Book },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
interface PluginInstalledComponentProps {
|
interface PluginInstalledComponentProps {
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ import { Separator } from '@/components/ui/separator';
|
|||||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
||||||
import {
|
import {
|
||||||
Search,
|
Search,
|
||||||
|
Puzzle,
|
||||||
|
Server,
|
||||||
|
Sparkles,
|
||||||
Wrench,
|
Wrench,
|
||||||
AudioWaveform,
|
AudioWaveform,
|
||||||
Hash,
|
Hash,
|
||||||
@@ -88,9 +91,9 @@ function MarketPageContent({
|
|||||||
|
|
||||||
const extensionTypeOptions = [
|
const extensionTypeOptions = [
|
||||||
{ value: 'all', label: t('market.filters.allFormats'), icon: null },
|
{ value: 'all', label: t('market.filters.allFormats'), icon: null },
|
||||||
{ value: 'plugin', label: t('market.typePlugin'), icon: Wrench },
|
{ value: 'plugin', label: t('market.typePlugin'), icon: Puzzle },
|
||||||
{ value: 'mcp', label: t('market.typeMCP'), icon: AudioWaveform },
|
{ value: 'mcp', label: t('market.typeMCP'), icon: Server },
|
||||||
{ value: 'skill', label: t('market.typeSkill'), icon: Book },
|
{ value: 'skill', label: t('market.typeSkill'), icon: Sparkles },
|
||||||
];
|
];
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export default function PluginMarketCardComponent({
|
|||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-label={t('market.installCard', { name: cardVO.label })}
|
aria-label={t('market.installCard', { name: cardVO.label })}
|
||||||
className="w-[100%] h-[10rem] cursor-pointer bg-white rounded-[10px] shadow-[0px_0px_4px_0_rgba(0,0,0,0.2)] p-3 sm:p-[1rem] hover:shadow-[0px_2px_8px_0_rgba(0,0,0,0.15)] transition-shadow duration-200 outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 dark:bg-[#1f1f22] dark:shadow-[0px_0px_4px_0_rgba(255,255,255,0.1)] dark:hover:shadow-[0px_2px_8px_0_rgba(255,255,255,0.15)] relative"
|
className="w-[100%] h-[10rem] cursor-pointer bg-white rounded-[10px] border border-border shadow-[0px_1px_2px_0_rgba(0,0,0,0.06)] p-3 sm:p-[1rem] hover:shadow-[0px_2px_5px_0_rgba(0,0,0,0.08)] transition-shadow duration-200 outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 dark:bg-[#1f1f22] dark:shadow-[0px_1px_2px_0_rgba(255,255,255,0.04)] dark:hover:shadow-[0px_2px_5px_0_rgba(255,255,255,0.07)] relative"
|
||||||
onClick={handleInstallClick}
|
onClick={handleInstallClick}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === 'Enter' || event.key === ' ') {
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const enUS = {
|
|||||||
helpDocs: 'Get Help',
|
helpDocs: 'Get Help',
|
||||||
featureRequest: 'Feature Request',
|
featureRequest: 'Feature Request',
|
||||||
starOnGitHub: 'Star on GitHub',
|
starOnGitHub: 'Star on GitHub',
|
||||||
|
joinDiscord: 'Join our Discord',
|
||||||
create: 'Create',
|
create: 'Create',
|
||||||
edit: 'Edit',
|
edit: 'Edit',
|
||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
@@ -631,8 +632,8 @@ const enUS = {
|
|||||||
},
|
},
|
||||||
market: {
|
market: {
|
||||||
searchPlaceholder: 'Search plugins...',
|
searchPlaceholder: 'Search plugins...',
|
||||||
searchResults: 'Found {{count}} plugins',
|
searchResults: 'Found {{count}} extensions',
|
||||||
totalPlugins: 'Total {{count}} plugins',
|
totalPlugins: 'Total {{count}} extensions',
|
||||||
noPlugins: 'No plugins available',
|
noPlugins: 'No plugins available',
|
||||||
noResults: 'No relevant plugins found',
|
noResults: 'No relevant plugins found',
|
||||||
loadingMore: 'Loading more...',
|
loadingMore: 'Loading more...',
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ const esES = {
|
|||||||
helpDocs: 'Obtener ayuda',
|
helpDocs: 'Obtener ayuda',
|
||||||
featureRequest: 'Solicitar función',
|
featureRequest: 'Solicitar función',
|
||||||
starOnGitHub: 'Dar estrella en GitHub',
|
starOnGitHub: 'Dar estrella en GitHub',
|
||||||
|
joinDiscord: 'Únete a Discord',
|
||||||
create: 'Crear',
|
create: 'Crear',
|
||||||
edit: 'Editar',
|
edit: 'Editar',
|
||||||
delete: 'Eliminar',
|
delete: 'Eliminar',
|
||||||
@@ -644,8 +645,8 @@ const esES = {
|
|||||||
},
|
},
|
||||||
market: {
|
market: {
|
||||||
searchPlaceholder: 'Buscar plugins...',
|
searchPlaceholder: 'Buscar plugins...',
|
||||||
searchResults: 'Se encontraron {{count}} plugins',
|
searchResults: 'Se encontraron {{count}} extensiones',
|
||||||
totalPlugins: 'Total {{count}} plugins',
|
totalPlugins: 'Total {{count}} extensiones',
|
||||||
noPlugins: 'No hay plugins disponibles',
|
noPlugins: 'No hay plugins disponibles',
|
||||||
noResults: 'No se encontraron plugins relevantes',
|
noResults: 'No se encontraron plugins relevantes',
|
||||||
loadingMore: 'Cargando más...',
|
loadingMore: 'Cargando más...',
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const jaJP = {
|
|||||||
helpDocs: 'ヘルプドキュメント',
|
helpDocs: 'ヘルプドキュメント',
|
||||||
featureRequest: '機能リクエスト',
|
featureRequest: '機能リクエスト',
|
||||||
starOnGitHub: 'GitHubでStarする',
|
starOnGitHub: 'GitHubでStarする',
|
||||||
|
joinDiscord: 'Discord に参加',
|
||||||
create: '作成',
|
create: '作成',
|
||||||
edit: '編集',
|
edit: '編集',
|
||||||
delete: '削除',
|
delete: '削除',
|
||||||
@@ -636,8 +637,8 @@ const jaJP = {
|
|||||||
},
|
},
|
||||||
market: {
|
market: {
|
||||||
searchPlaceholder: 'プラグインを検索...',
|
searchPlaceholder: 'プラグインを検索...',
|
||||||
searchResults: '{{count}} 個のプラグインが見つかりました',
|
searchResults: '{{count}} 個の拡張機能が見つかりました',
|
||||||
totalPlugins: '合計 {{count}} 個のプラグイン',
|
totalPlugins: '合計 {{count}} 個の拡張機能',
|
||||||
noPlugins: '利用可能なプラグインがありません',
|
noPlugins: '利用可能なプラグインがありません',
|
||||||
noResults: '関連するプラグインが見つかりません',
|
noResults: '関連するプラグインが見つかりません',
|
||||||
loadingMore: 'さらに読み込み中...',
|
loadingMore: 'さらに読み込み中...',
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const ruRU = {
|
|||||||
helpDocs: 'Помощь',
|
helpDocs: 'Помощь',
|
||||||
featureRequest: 'Запрос функции',
|
featureRequest: 'Запрос функции',
|
||||||
starOnGitHub: 'Поставить звезду на GitHub',
|
starOnGitHub: 'Поставить звезду на GitHub',
|
||||||
|
joinDiscord: 'Присоединиться к Discord',
|
||||||
create: 'Создать',
|
create: 'Создать',
|
||||||
edit: 'Редактировать',
|
edit: 'Редактировать',
|
||||||
delete: 'Удалить',
|
delete: 'Удалить',
|
||||||
@@ -642,8 +643,8 @@ const ruRU = {
|
|||||||
},
|
},
|
||||||
market: {
|
market: {
|
||||||
searchPlaceholder: 'Поиск плагинов...',
|
searchPlaceholder: 'Поиск плагинов...',
|
||||||
searchResults: 'Найдено {{count}} плагинов',
|
searchResults: 'Найдено {{count}} расширений',
|
||||||
totalPlugins: 'Всего {{count}} плагинов',
|
totalPlugins: 'Всего {{count}} расширений',
|
||||||
noPlugins: 'Нет доступных плагинов',
|
noPlugins: 'Нет доступных плагинов',
|
||||||
noResults: 'Подходящие плагины не найдены',
|
noResults: 'Подходящие плагины не найдены',
|
||||||
loadingMore: 'Загрузка ещё...',
|
loadingMore: 'Загрузка ещё...',
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const thTH = {
|
|||||||
helpDocs: 'ขอความช่วยเหลือ',
|
helpDocs: 'ขอความช่วยเหลือ',
|
||||||
featureRequest: 'ขอฟีเจอร์ใหม่',
|
featureRequest: 'ขอฟีเจอร์ใหม่',
|
||||||
starOnGitHub: 'ให้ดาวบน GitHub',
|
starOnGitHub: 'ให้ดาวบน GitHub',
|
||||||
|
joinDiscord: 'เข้าร่วม Discord',
|
||||||
create: 'สร้าง',
|
create: 'สร้าง',
|
||||||
edit: 'แก้ไข',
|
edit: 'แก้ไข',
|
||||||
delete: 'ลบ',
|
delete: 'ลบ',
|
||||||
@@ -623,8 +624,8 @@ const thTH = {
|
|||||||
},
|
},
|
||||||
market: {
|
market: {
|
||||||
searchPlaceholder: 'ค้นหาปลั๊กอิน...',
|
searchPlaceholder: 'ค้นหาปลั๊กอิน...',
|
||||||
searchResults: 'พบ {{count}} ปลั๊กอิน',
|
searchResults: 'พบ {{count}} ส่วนขยาย',
|
||||||
totalPlugins: 'ทั้งหมด {{count}} ปลั๊กอิน',
|
totalPlugins: 'ทั้งหมด {{count}} ส่วนขยาย',
|
||||||
noPlugins: 'ไม่มีปลั๊กอินที่พร้อมใช้งาน',
|
noPlugins: 'ไม่มีปลั๊กอินที่พร้อมใช้งาน',
|
||||||
noResults: 'ไม่พบปลั๊กอินที่เกี่ยวข้อง',
|
noResults: 'ไม่พบปลั๊กอินที่เกี่ยวข้อง',
|
||||||
loadingMore: 'กำลังโหลดเพิ่มเติม...',
|
loadingMore: 'กำลังโหลดเพิ่มเติม...',
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const viVN = {
|
|||||||
helpDocs: 'Trợ giúp',
|
helpDocs: 'Trợ giúp',
|
||||||
featureRequest: 'Yêu cầu tính năng',
|
featureRequest: 'Yêu cầu tính năng',
|
||||||
starOnGitHub: 'Star trên GitHub',
|
starOnGitHub: 'Star trên GitHub',
|
||||||
|
joinDiscord: 'Tham gia Discord',
|
||||||
create: 'Tạo',
|
create: 'Tạo',
|
||||||
edit: 'Chỉnh sửa',
|
edit: 'Chỉnh sửa',
|
||||||
delete: 'Xóa',
|
delete: 'Xóa',
|
||||||
@@ -637,8 +638,8 @@ const viVN = {
|
|||||||
},
|
},
|
||||||
market: {
|
market: {
|
||||||
searchPlaceholder: 'Tìm kiếm plugin...',
|
searchPlaceholder: 'Tìm kiếm plugin...',
|
||||||
searchResults: 'Tìm thấy {{count}} plugin',
|
searchResults: 'Tìm thấy {{count}} tiện ích mở rộng',
|
||||||
totalPlugins: 'Tổng cộng {{count}} plugin',
|
totalPlugins: 'Tổng cộng {{count}} tiện ích mở rộng',
|
||||||
noPlugins: 'Không có plugin nào',
|
noPlugins: 'Không có plugin nào',
|
||||||
noResults: 'Không tìm thấy plugin liên quan',
|
noResults: 'Không tìm thấy plugin liên quan',
|
||||||
loadingMore: 'Đang tải thêm...',
|
loadingMore: 'Đang tải thêm...',
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const zhHans = {
|
|||||||
helpDocs: '帮助文档',
|
helpDocs: '帮助文档',
|
||||||
featureRequest: '需求建议',
|
featureRequest: '需求建议',
|
||||||
starOnGitHub: '在 GitHub 上 Star',
|
starOnGitHub: '在 GitHub 上 Star',
|
||||||
|
joinDiscord: '加入 Discord 社区',
|
||||||
create: '创建',
|
create: '创建',
|
||||||
edit: '编辑',
|
edit: '编辑',
|
||||||
delete: '删除',
|
delete: '删除',
|
||||||
@@ -605,8 +606,8 @@ const zhHans = {
|
|||||||
},
|
},
|
||||||
market: {
|
market: {
|
||||||
searchPlaceholder: '搜索插件...',
|
searchPlaceholder: '搜索插件...',
|
||||||
searchResults: '搜索到 {{count}} 个插件',
|
searchResults: '搜索到 {{count}} 个扩展',
|
||||||
totalPlugins: '共 {{count}} 个插件',
|
totalPlugins: '共 {{count}} 个扩展',
|
||||||
noPlugins: '暂无插件',
|
noPlugins: '暂无插件',
|
||||||
noResults: '未找到相关插件',
|
noResults: '未找到相关插件',
|
||||||
loadingMore: '加载更多...',
|
loadingMore: '加载更多...',
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const zhHant = {
|
|||||||
helpDocs: '輔助說明',
|
helpDocs: '輔助說明',
|
||||||
featureRequest: '需求建議',
|
featureRequest: '需求建議',
|
||||||
starOnGitHub: '在 GitHub 上 Star',
|
starOnGitHub: '在 GitHub 上 Star',
|
||||||
|
joinDiscord: '加入 Discord 社群',
|
||||||
create: '建立',
|
create: '建立',
|
||||||
edit: '編輯',
|
edit: '編輯',
|
||||||
delete: '刪除',
|
delete: '刪除',
|
||||||
@@ -605,8 +606,8 @@ const zhHant = {
|
|||||||
},
|
},
|
||||||
market: {
|
market: {
|
||||||
searchPlaceholder: '搜尋插件...',
|
searchPlaceholder: '搜尋插件...',
|
||||||
searchResults: '搜尋到 {{count}} 個插件',
|
searchResults: '搜尋到 {{count}} 個擴展',
|
||||||
totalPlugins: '共 {{count}} 個插件',
|
totalPlugins: '共 {{count}} 個擴展',
|
||||||
noPlugins: '暫無插件',
|
noPlugins: '暫無插件',
|
||||||
noResults: '未找到相關插件',
|
noResults: '未找到相關插件',
|
||||||
loadingMore: '載入更多...',
|
loadingMore: '載入更多...',
|
||||||
|
|||||||
Reference in New Issue
Block a user