refactor(frontend): remove unused modules

This commit is contained in:
Tunglies
2026-07-12 12:42:27 +08:00
parent 656b6b6d44
commit 14bc636ee8
38 changed files with 6 additions and 797 deletions
+4 -7
View File
@@ -21,15 +21,13 @@ src/locales/
home.json
shared.json
...
index.ts
zh/
...
```
- JSON files map to namespaces (for example `home.json` → `home.*`). Keep keys scoped to the file they belong to.
- `shared.json` stores reusable vocabulary (buttons, validations, etc.); feature-specific wording should live in the relevant namespace.
- `index.ts` re-exports a `resources` object that aggregates the namespace JSON files. When adding or removing namespaces, mirror the pattern from `src/locales/en/index.ts`.
- Frontend bundles are lazy-loaded by `src/services/i18n.ts`. Only languages listed in `supportedLanguages` are fetched at runtime, so append new codes there when you add a locale.
- `src/services/i18n.ts` automatically discovers and lazy-loads each locale's JSON sections, so no aggregate module is required. Keep locales aligned with `src/locales/en/`, and add new codes to `supportedLanguages`.
Because backend translations now live in their own directory, you no longer need to run `pnpm prebuild` just to sync locales—the frontend folder is the sole source of truth for web bundles.
@@ -52,10 +50,9 @@ Native UI strings (tray menu, notifications, dialogs) use `rust-i18n` with YAML
## Adding a new language
1. Duplicate `src/locales/en/` into `src/locales/<new-lang>/` and translate the JSON files while preserving key structure.
2. Update the locale’s `index.ts` to import every namespace. Matching the English file is the easiest way to avoid missing exports.
3. Append the language code to `supportedLanguages` in `src/services/i18n.ts`.
4. If the backend should expose the language, create `crates/clash-verge-i18n/<new-lang>.yml` and translate the keys used in existing YAML files.
5. Run `pnpm i18n:format`, `pnpm i18n:types`, and (optionally) `pnpm i18n:check` in dry-run mode to confirm structure.
2. Append the language code to `supportedLanguages` in `src/services/i18n.ts`.
3. If the backend should expose the language, create `crates/clash-verge-i18n/<new-lang>.yml` and translate the keys used in existing YAML files.
4. Run `pnpm i18n:format`, `pnpm i18n:types`, and (optionally) `pnpm i18n:check` in dry-run mode to confirm structure.
## Authoring guidelines
+2 -62
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env node
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
@@ -1020,7 +1018,7 @@ function loadBackendLocales() {
const localePath = path.join(BACKEND_LOCALES_DIR, entry.name)
const name = entry.name.replace(/\.(ya?ml)$/i, '')
const raw = fs.readFileSync(localePath, 'utf8')
let data = {}
let data
try {
const parsed = yaml.load(raw)
data = isPlainObject(parsed) ? parsed : {}
@@ -1055,6 +1053,7 @@ function ensureBackup(localePath) {
} catch (error) {
throw new Error(
`Failed to recycle existing backup for ${path.basename(localePath)}: ${error.message}`,
{ cause: error },
)
}
}
@@ -1086,59 +1085,6 @@ function cleanupBackups(backups) {
backups.clear()
}
function toModuleIdentifier(namespace, seen) {
const RESERVED = new Set([
'default',
'function',
'var',
'let',
'const',
'import',
'export',
'class',
'enum',
])
const base =
namespace.replace(/[^a-zA-Z0-9_$]/g, '_').replace(/^[^a-zA-Z_$]+/, '') ||
'ns'
let candidate = base
let counter = 1
while (RESERVED.has(candidate) || seen.has(candidate)) {
candidate = `${base}_${counter}`
counter += 1
}
seen.add(candidate)
return candidate
}
function regenerateLocaleIndex(localeDir, namespaces) {
const seen = new Set()
const imports = []
const mappings = []
for (const namespace of namespaces) {
const filePath = path.join(localeDir, `${namespace}.json`)
if (!fs.existsSync(filePath)) continue
const identifier = toModuleIdentifier(namespace, seen)
const key = namespace === identifier ? namespace : `'${namespace}'`
imports.push(`import ${identifier} from './${namespace}.json'`)
mappings.push(` ${key}: ${identifier},`)
}
const content = `${imports.join('\n')}
const resources = {
${mappings.join('\n')}
}
export default resources
`
fs.writeFileSync(path.join(localeDir, 'index.ts'), content, 'utf8')
}
function writeLocale(locale, data, options) {
const backups = new Map()
let success = false
@@ -1163,7 +1109,6 @@ function writeLocale(locale, data, options) {
}
const entries = Object.entries(data)
const orderedNamespaces = entries.map(([namespace]) => namespace)
const existingFiles = new Map(
locale.files.map((file) => [file.namespace, file.path]),
)
@@ -1187,11 +1132,6 @@ function writeLocale(locale, data, options) {
}
}
regenerateLocaleIndex(locale.dir, orderedNamespaces)
locale.files = orderedNamespaces.map((namespace) => ({
namespace,
path: path.join(locale.dir, `${namespace}.json`),
}))
success = true
} finally {
if (success) {
@@ -1,91 +0,0 @@
import { CloseRounded } from '@mui/icons-material'
import {
styled,
ListItem,
IconButton,
ListItemText,
Box,
alpha,
} from '@mui/material'
import { useLockFn } from 'ahooks'
import dayjs from 'dayjs'
import { useTranslation } from 'react-i18next'
import { closeConnection } from 'tauri-plugin-mihomo-api'
import parseTraffic from '@/utils/parse-traffic'
const Tag = styled('span')(({ theme }) => ({
fontSize: '10px',
padding: '0 4px',
lineHeight: 1.375,
border: '1px solid',
borderRadius: 4,
borderColor: alpha(theme.palette.text.secondary, 0.35),
marginTop: '4px',
marginRight: '4px',
}))
interface Props {
value: IConnectionsItem
closed: boolean
onShowDetail?: () => void
}
export const ConnectionItem = (props: Props) => {
const { value, closed, onShowDetail } = props
const { id, metadata, chains, start, curUpload, curDownload } = value
const { t } = useTranslation()
const onDelete = useLockFn(async () => closeConnection(id))
const showTraffic = curUpload! >= 100 || curDownload! >= 100
return (
<ListItem
dense
sx={{ borderBottom: '1px solid var(--divider-color)' }}
secondaryAction={
!closed && (
<IconButton
edge="end"
color="inherit"
onClick={onDelete}
title={t('connections.components.actions.closeConnection')}
aria-label={t('connections.components.actions.closeConnection')}
>
<CloseRounded />
</IconButton>
)
}
>
<ListItemText
sx={{ userSelect: 'text', cursor: 'pointer' }}
primary={metadata.host || metadata.destinationIP}
onClick={onShowDetail}
secondary={
<Box sx={{ display: 'flex', flexWrap: 'wrap' }}>
<Tag sx={{ textTransform: 'uppercase', color: 'success' }}>
{metadata.network}
</Tag>
<Tag>{metadata.type}</Tag>
{!!metadata.process && <Tag>{metadata.process}</Tag>}
{chains?.length > 0 && (
<Tag>{[...chains].reverse().join(' / ')}</Tag>
)}
<Tag>{dayjs(start).fromNow()}</Tag>
{showTraffic && (
<Tag>
{parseTraffic(curUpload!)} / {parseTraffic(curDownload!)}
</Tag>
)}
</Box>
}
/>
</ListItem>
)
}
@@ -1,52 +0,0 @@
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
TextField,
} from '@mui/material'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
interface Props {
onConfirm: (passwd: string) => Promise<void>
}
export const PasswordInput = (props: Props) => {
const { onConfirm } = props
const { t } = useTranslation()
const [passwd, setPasswd] = useState('')
return (
<Dialog open={true} maxWidth="xs" fullWidth>
<DialogTitle>
{t('settings.modals.password.prompts.enterRoot')}
</DialogTitle>
<DialogContent>
<TextField
sx={{ mt: 1 }}
autoFocus
label={t('shared.labels.password')}
fullWidth
size="small"
type="password"
value={passwd}
onKeyDown={(e) => e.key === 'Enter' && onConfirm(passwd)}
onChange={(e) => setPasswd(e.target.value)}
></TextField>
</DialogContent>
<DialogActions>
<Button
onClick={async () => await onConfirm(passwd)}
variant="contained"
>
{t('shared.actions.confirm')}
</Button>
</DialogActions>
</Dialog>
)
}
-82
View File
@@ -1,82 +0,0 @@
import { useMemo } from 'react'
import {
useAppRefreshers,
useClashConfigData,
useProxiesData,
} from '@/providers/app-data-context'
// 定义代理组类型
interface ProxyGroup {
name: string
now: string
}
// 获取当前代理节点信息的自定义Hook
export const useCurrentProxy = () => {
// 从AppDataProvider获取数据
const { proxies } = useProxiesData()
const { clashConfig } = useClashConfigData()
const { refreshProxy } = useAppRefreshers()
// 获取当前模式
const currentMode = clashConfig?.mode?.toLowerCase() || 'rule'
// 获取当前代理节点信息
const currentProxyInfo = useMemo(() => {
if (!proxies) return { currentProxy: null, primaryGroupName: null }
const { global, groups, records } = proxies
// 默认信息
let primaryGroupName = 'GLOBAL'
let currentName = global?.now
// 在规则模式下,寻找主要代理组(通常是第一个或者名字包含特定关键词的组)
if (currentMode === 'rule' && groups.length > 0) {
// 查找主要的代理组(优先级:包含关键词 > 第一个非GLOBAL组)
const primaryKeywords = [
'auto',
'select',
'proxy',
'节点选择',
'自动选择',
]
const primaryGroup =
groups.find((group: ProxyGroup) =>
primaryKeywords.some((keyword) =>
group.name.toLowerCase().includes(keyword.toLowerCase()),
),
) || groups.filter((g: ProxyGroup) => g.name !== 'GLOBAL')[0]
if (primaryGroup) {
primaryGroupName = primaryGroup.name
currentName = primaryGroup.now
}
}
// 如果找不到当前节点,返回null
if (!currentName) return { currentProxy: null, primaryGroupName }
// 获取完整的节点信息
const currentProxy = records[currentName] || {
name: currentName,
type: 'Unknown',
udp: false,
xudp: false,
tfo: false,
mptcp: false,
smux: false,
history: [],
}
return { currentProxy, primaryGroupName }
}, [proxies, currentMode])
return {
currentProxy: currentProxyInfo.currentProxy,
primaryGroupName: currentProxyInfo.primaryGroupName,
mode: currentMode,
refreshProxy,
}
}
-25
View File
@@ -1,25 +0,0 @@
import connections from './connections.json'
import home from './home.json'
import layout from './layout.json'
import logs from './logs.json'
import profiles from './profiles.json'
import proxies from './proxies.json'
import rules from './rules.json'
import settings from './settings.json'
import shared from './shared.json'
import tests from './tests.json'
const resources = {
connections: connections,
home: home,
layout: layout,
logs: logs,
profiles: profiles,
proxies: proxies,
rules: rules,
settings: settings,
shared: shared,
tests: tests,
}
export default resources
-5
View File
@@ -609,11 +609,6 @@
"reactivateProfiles": "إعادة تنشيط الملفات الشخصية"
}
},
"password": {
"prompts": {
"enterRoot": "يرجى إدخال كلمة مرور الرووت"
}
},
"networkInterface": {
"title": "واجهة الشبكة",
"fields": {
-25
View File
@@ -1,25 +0,0 @@
import connections from './connections.json'
import home from './home.json'
import layout from './layout.json'
import logs from './logs.json'
import profiles from './profiles.json'
import proxies from './proxies.json'
import rules from './rules.json'
import settings from './settings.json'
import shared from './shared.json'
import tests from './tests.json'
const resources = {
connections: connections,
home: home,
layout: layout,
logs: logs,
profiles: profiles,
proxies: proxies,
rules: rules,
settings: settings,
shared: shared,
tests: tests,
}
export default resources
-5
View File
@@ -609,11 +609,6 @@
"reactivateProfiles": "Abonnement erneut aktivieren"
}
},
"password": {
"prompts": {
"enterRoot": "Bitte geben Sie Ihr Root-Passwort ein."
}
},
"networkInterface": {
"title": "Netzwerkschnittstelle",
"fields": {
-25
View File
@@ -1,25 +0,0 @@
import connections from './connections.json'
import home from './home.json'
import layout from './layout.json'
import logs from './logs.json'
import profiles from './profiles.json'
import proxies from './proxies.json'
import rules from './rules.json'
import settings from './settings.json'
import shared from './shared.json'
import tests from './tests.json'
const resources = {
connections: connections,
home: home,
layout: layout,
logs: logs,
profiles: profiles,
proxies: proxies,
rules: rules,
settings: settings,
shared: shared,
tests: tests,
}
export default resources
-5
View File
@@ -609,11 +609,6 @@
"reactivateProfiles": "Reactivate Profiles"
}
},
"password": {
"prompts": {
"enterRoot": "Please enter your root password"
}
},
"networkInterface": {
"title": "Network Interface",
"fields": {
-25
View File
@@ -1,25 +0,0 @@
import connections from './connections.json'
import home from './home.json'
import layout from './layout.json'
import logs from './logs.json'
import profiles from './profiles.json'
import proxies from './proxies.json'
import rules from './rules.json'
import settings from './settings.json'
import shared from './shared.json'
import tests from './tests.json'
const resources = {
connections: connections,
home: home,
layout: layout,
logs: logs,
profiles: profiles,
proxies: proxies,
rules: rules,
settings: settings,
shared: shared,
tests: tests,
}
export default resources
-5
View File
@@ -609,11 +609,6 @@
"reactivateProfiles": "Reactivar suscripciones"
}
},
"password": {
"prompts": {
"enterRoot": "Ingrese su contraseña de root"
}
},
"networkInterface": {
"title": "Interfaz de red",
"fields": {
-25
View File
@@ -1,25 +0,0 @@
import connections from './connections.json'
import home from './home.json'
import layout from './layout.json'
import logs from './logs.json'
import profiles from './profiles.json'
import proxies from './proxies.json'
import rules from './rules.json'
import settings from './settings.json'
import shared from './shared.json'
import tests from './tests.json'
const resources = {
connections: connections,
home: home,
layout: layout,
logs: logs,
profiles: profiles,
proxies: proxies,
rules: rules,
settings: settings,
shared: shared,
tests: tests,
}
export default resources
-5
View File
@@ -609,11 +609,6 @@
"reactivateProfiles": "فعال‌سازی مجدد پروفایل‌ها"
}
},
"password": {
"prompts": {
"enterRoot": "لطفاً رمز ریشه خود را وارد کنید"
}
},
"networkInterface": {
"title": "رابط شبکه",
"fields": {
-25
View File
@@ -1,25 +0,0 @@
import connections from './connections.json'
import home from './home.json'
import layout from './layout.json'
import logs from './logs.json'
import profiles from './profiles.json'
import proxies from './proxies.json'
import rules from './rules.json'
import settings from './settings.json'
import shared from './shared.json'
import tests from './tests.json'
const resources = {
connections: connections,
home: home,
layout: layout,
logs: logs,
profiles: profiles,
proxies: proxies,
rules: rules,
settings: settings,
shared: shared,
tests: tests,
}
export default resources
-5
View File
@@ -609,11 +609,6 @@
"reactivateProfiles": "Reaktivasi Profil"
}
},
"password": {
"prompts": {
"enterRoot": "Harap masukkan kata sandi root Anda"
}
},
"networkInterface": {
"title": "Antarmuka Jaringan",
"fields": {
-25
View File
@@ -1,25 +0,0 @@
import connections from './connections.json'
import home from './home.json'
import layout from './layout.json'
import logs from './logs.json'
import profiles from './profiles.json'
import proxies from './proxies.json'
import rules from './rules.json'
import settings from './settings.json'
import shared from './shared.json'
import tests from './tests.json'
const resources = {
connections: connections,
home: home,
layout: layout,
logs: logs,
profiles: profiles,
proxies: proxies,
rules: rules,
settings: settings,
shared: shared,
tests: tests,
}
export default resources
-5
View File
@@ -609,11 +609,6 @@
"reactivateProfiles": "プロファイルを再アクティブ化"
}
},
"password": {
"prompts": {
"enterRoot": "ルートパスワードを入力してください。"
}
},
"networkInterface": {
"title": "ネットワークインターフェース",
"fields": {
-25
View File
@@ -1,25 +0,0 @@
import connections from './connections.json'
import home from './home.json'
import layout from './layout.json'
import logs from './logs.json'
import profiles from './profiles.json'
import proxies from './proxies.json'
import rules from './rules.json'
import settings from './settings.json'
import shared from './shared.json'
import tests from './tests.json'
const resources = {
connections: connections,
home: home,
layout: layout,
logs: logs,
profiles: profiles,
proxies: proxies,
rules: rules,
settings: settings,
shared: shared,
tests: tests,
}
export default resources
-5
View File
@@ -609,11 +609,6 @@
"reactivateProfiles": "프로필 재활성화"
}
},
"password": {
"prompts": {
"enterRoot": "루트 암호를 입력하세요"
}
},
"networkInterface": {
"title": "네트워크 인터페이스",
"fields": {
-25
View File
@@ -1,25 +0,0 @@
import connections from './connections.json'
import home from './home.json'
import layout from './layout.json'
import logs from './logs.json'
import profiles from './profiles.json'
import proxies from './proxies.json'
import rules from './rules.json'
import settings from './settings.json'
import shared from './shared.json'
import tests from './tests.json'
const resources = {
connections: connections,
home: home,
layout: layout,
logs: logs,
profiles: profiles,
proxies: proxies,
rules: rules,
settings: settings,
shared: shared,
tests: tests,
}
export default resources
-5
View File
@@ -609,11 +609,6 @@
"reactivateProfiles": "Перезапустить профиль"
}
},
"password": {
"prompts": {
"enterRoot": "Пожалуйста, введите пароль root"
}
},
"networkInterface": {
"title": "Сетевой интерфейс",
"fields": {
-25
View File
@@ -1,25 +0,0 @@
import connections from './connections.json'
import home from './home.json'
import layout from './layout.json'
import logs from './logs.json'
import profiles from './profiles.json'
import proxies from './proxies.json'
import rules from './rules.json'
import settings from './settings.json'
import shared from './shared.json'
import tests from './tests.json'
const resources = {
connections: connections,
home: home,
layout: layout,
logs: logs,
profiles: profiles,
proxies: proxies,
rules: rules,
settings: settings,
shared: shared,
tests: tests,
}
export default resources
-5
View File
@@ -609,11 +609,6 @@
"reactivateProfiles": "Profilleri Yeniden Etkinleştir"
}
},
"password": {
"prompts": {
"enterRoot": "Lütfen root şifrenizi girin"
}
},
"networkInterface": {
"title": "Ağ Arayüzü",
"fields": {
-25
View File
@@ -1,25 +0,0 @@
import connections from './connections.json'
import home from './home.json'
import layout from './layout.json'
import logs from './logs.json'
import profiles from './profiles.json'
import proxies from './proxies.json'
import rules from './rules.json'
import settings from './settings.json'
import shared from './shared.json'
import tests from './tests.json'
const resources = {
connections: connections,
home: home,
layout: layout,
logs: logs,
profiles: profiles,
proxies: proxies,
rules: rules,
settings: settings,
shared: shared,
tests: tests,
}
export default resources
-5
View File
@@ -609,11 +609,6 @@
"reactivateProfiles": "Профильләрне янәдән активлаштыру"
}
},
"password": {
"prompts": {
"enterRoot": "root паролен языгыз"
}
},
"networkInterface": {
"title": "Челтәр интерфейсы",
"fields": {
-25
View File
@@ -1,25 +0,0 @@
import connections from './connections.json'
import home from './home.json'
import layout from './layout.json'
import logs from './logs.json'
import profiles from './profiles.json'
import proxies from './proxies.json'
import rules from './rules.json'
import settings from './settings.json'
import shared from './shared.json'
import tests from './tests.json'
const resources = {
connections: connections,
home: home,
layout: layout,
logs: logs,
profiles: profiles,
proxies: proxies,
rules: rules,
settings: settings,
shared: shared,
tests: tests,
}
export default resources
-5
View File
@@ -609,11 +609,6 @@
"reactivateProfiles": "重新激活订阅"
}
},
"password": {
"prompts": {
"enterRoot": "请输入您的 root 密码"
}
},
"networkInterface": {
"title": "网络接口",
"fields": {
-25
View File
@@ -1,25 +0,0 @@
import connections from './connections.json'
import home from './home.json'
import layout from './layout.json'
import logs from './logs.json'
import profiles from './profiles.json'
import proxies from './proxies.json'
import rules from './rules.json'
import settings from './settings.json'
import shared from './shared.json'
import tests from './tests.json'
const resources = {
connections: connections,
home: home,
layout: layout,
logs: logs,
profiles: profiles,
proxies: proxies,
rules: rules,
settings: settings,
shared: shared,
tests: tests,
}
export default resources
-5
View File
@@ -609,11 +609,6 @@
"reactivateProfiles": "重新啟用訂閱"
}
},
"password": {
"prompts": {
"enterRoot": "請輸入您的 root 密碼"
}
},
"networkInterface": {
"title": "網路介面",
"fields": {
-20
View File
@@ -1,20 +0,0 @@
import { createContext, use } from 'react'
export interface ChainProxyContextType {
isChainMode: boolean
setChainMode: (isChain: boolean) => void
chainConfigData: string | null
setChainConfigData: (data: string | null) => void
}
export const ChainProxyContext = createContext<ChainProxyContextType | null>(
null,
)
export const useChainProxy = () => {
const context = use(ChainProxyContext)
if (!context) {
throw new Error('useChainProxy must be used within a ChainProxyProvider')
}
return context
}
-32
View File
@@ -1,32 +0,0 @@
import React, { useCallback, useMemo, useState } from 'react'
import { ChainProxyContext } from './chain-proxy-context'
export const ChainProxyProvider = ({
children,
}: {
children: React.ReactNode
}) => {
const [isChainMode, setIsChainMode] = useState(false)
const [chainConfigData, setChainConfigData] = useState<string | null>(null)
const setChainMode = useCallback((isChain: boolean) => {
setIsChainMode(isChain)
}, [])
const setChainConfigDataCallback = useCallback((data: string | null) => {
setChainConfigData(data)
}, [])
const contextValue = useMemo(
() => ({
isChainMode,
setChainMode,
chainConfigData,
setChainConfigData: setChainConfigDataCallback,
}),
[isChainMode, setChainMode, chainConfigData, setChainConfigDataCallback],
)
return <ChainProxyContext value={contextValue}>{children}</ChainProxyContext>
}
-35
View File
@@ -1,35 +0,0 @@
const SWR_NOT_SMART = {
revalidateOnFocus: false,
revalidateOnReconnect: false,
revalidateIfStale: false,
suspense: false,
errorRetryCount: 2,
dedupingInterval: 1500,
errorRetryInterval: 3000,
} as const
export const SWR_DEFAULTS = {
revalidateOnFocus: false,
revalidateOnReconnect: false,
suspense: false,
errorRetryCount: 2,
dedupingInterval: 5000,
} as const
export const SWR_SLOW_POLL = {
...SWR_DEFAULTS,
refreshInterval: 60000,
} as const
export const SWR_MIHOMO = {
...SWR_NOT_SMART,
errorRetryInterval: 2000,
errorRetryCount: 3,
}
export const SWR_EXTERNAL_API = {
...SWR_NOT_SMART,
shouldRetryOnError: true,
errorRetryCount: 1,
errorRetryInterval: 30_000,
} as const
-1
View File
@@ -672,7 +672,6 @@ export const translationKeys = [
'settings.modals.hotkey.functions.entryLightweightMode',
'settings.modals.hotkey.functions.direct',
'settings.modals.hotkey.functions.reactivateProfiles',
'settings.modals.password.prompts.enterRoot',
'settings.modals.networkInterface.title',
'settings.modals.networkInterface.fields.ipAddress',
'settings.modals.networkInterface.fields.macAddress',
-5
View File
@@ -981,11 +981,6 @@ export interface TranslationResources {
}
title: string
}
password: {
prompts: {
enterRoot: string
}
}
sysproxy: {
actions: {
editPac: string
-14
View File
@@ -1,14 +0,0 @@
// Deep copy and change all keys to lowercase
type TData = Record<string, any>
export default function ignoreCase(data: TData): TData {
if (!data) return {}
const newData = {} as TData
Object.entries(data).forEach(([key, value]) => {
newData[key.toLowerCase()] = JSON.parse(JSON.stringify(value))
})
return newData
}
-6
View File
@@ -1,6 +0,0 @@
export const truncateStr = (str?: string, prefixLen = 16, maxLen = 56) => {
if (!str || str.length <= maxLen) return str
return (
str.slice(0, prefixLen) + ' ... ' + str.slice(-(maxLen - prefixLen - 5))
)
}