refactor(translate): centralize language label handling with getLanguageLabel

Move language label generation from individual TranslateLanguage objects to a centralized getLanguageLabel function in useTranslate hook. This improves maintainability by removing duplicate label logic and makes it easier to update language labels globally.

- Remove label() method from TranslateLanguage type and all language objects
- Add getLanguageLabel function in useTranslate that handles label generation
- Update all components to use getLanguageLabel instead of label()
- Add labelMap for common language codes to avoid unnecessary lookups
This commit is contained in:
icarus 2025-10-15 00:15:48 +08:00
parent dbfece3590
commit 6cda7f891d
12 changed files with 107 additions and 88 deletions

View File

@ -18,19 +18,22 @@ type Props = {
} & Omit<SelectProps, 'labelRender' | 'options'>
const LanguageSelect = (props: Props) => {
const { translateLanguages } = useTranslate()
const { translateLanguages, getLanguageLabel } = useTranslate()
const { extraOptionsAfter, extraOptionsBefore, languageRenderer, ...restProps } = props
const defaultLanguageRenderer = useCallback((lang: TranslateLanguage) => {
return (
<Space.Compact direction="horizontal" block>
<span role="img" aria-label={lang.emoji} style={{ marginRight: 8 }}>
{lang.emoji}
</span>
{lang.label()}
</Space.Compact>
)
}, [])
const defaultLanguageRenderer = useCallback(
(lang: TranslateLanguage) => {
return (
<Space.Compact direction="horizontal" block>
<span role="img" aria-label={lang.emoji} style={{ marginRight: 8 }}>
{lang.emoji}
</span>
{getLanguageLabel(lang.langCode)}
</Space.Compact>
)
},
[getLanguageLabel]
)
const labelRender = (props) => {
const { label } = props

View File

@ -24,7 +24,7 @@ const TranslateButton: FC<Props> = ({ text, onTranslated, disabled, style, isLoa
const [isTranslating, setIsTranslating] = useState(false)
const [targetLanguage] = usePreference('feature.translate.target_language')
const [showTranslateConfirm] = usePreference('chat.input.translate.show_confirm')
const { getLanguageByLangcode } = useTranslate()
const { getLanguageLabel, getLanguageByLangcode } = useTranslate()
const translateConfirm = () => {
if (!showTranslateConfirm) {
@ -64,9 +64,7 @@ const TranslateButton: FC<Props> = ({ text, onTranslated, disabled, style, isLoa
}, [isLoading])
return (
<Tooltip
content={t('chat.input.translate', { target_language: getLanguageByLangcode(targetLanguage).label() })}
closeDelay={0}>
<Tooltip content={t('chat.input.translate', { target_language: getLanguageLabel(targetLanguage) })} closeDelay={0}>
<Button
onPress={handleTranslate}
isDisabled={disabled || isTranslating}

View File

@ -1,150 +1,128 @@
import i18n from '@renderer/i18n'
import type { TranslateLanguage } from '@renderer/types'
export const UNKNOWN: TranslateLanguage = {
value: 'Unknown',
langCode: 'unknown',
label: () => i18n.t('languages.unknown'),
emoji: '🏳️'
}
export const ENGLISH: TranslateLanguage = {
value: 'English',
langCode: 'en-us',
label: () => i18n.t('languages.english'),
emoji: '🇬🇧'
}
export const CHINESE_SIMPLIFIED: TranslateLanguage = {
value: 'Chinese (Simplified)',
langCode: 'zh-cn',
label: () => i18n.t('languages.chinese'),
emoji: '🇨🇳'
}
export const CHINESE_TRADITIONAL: TranslateLanguage = {
value: 'Chinese (Traditional)',
langCode: 'zh-tw',
label: () => i18n.t('languages.chinese-traditional'),
emoji: '🇭🇰'
}
export const JAPANESE: TranslateLanguage = {
value: 'Japanese',
langCode: 'ja-jp',
label: () => i18n.t('languages.japanese'),
emoji: '🇯🇵'
}
export const KOREAN: TranslateLanguage = {
value: 'Korean',
langCode: 'ko-kr',
label: () => i18n.t('languages.korean'),
emoji: '🇰🇷'
}
export const FRENCH: TranslateLanguage = {
value: 'French',
langCode: 'fr-fr',
label: () => i18n.t('languages.french'),
emoji: '🇫🇷'
}
export const GERMAN: TranslateLanguage = {
value: 'German',
langCode: 'de-de',
label: () => i18n.t('languages.german'),
emoji: '🇩🇪'
}
export const ITALIAN: TranslateLanguage = {
value: 'Italian',
langCode: 'it-it',
label: () => i18n.t('languages.italian'),
emoji: '🇮🇹'
}
export const SPANISH: TranslateLanguage = {
value: 'Spanish',
langCode: 'es-es',
label: () => i18n.t('languages.spanish'),
emoji: '🇪🇸'
}
export const PORTUGUESE: TranslateLanguage = {
value: 'Portuguese',
langCode: 'pt-pt',
label: () => i18n.t('languages.portuguese'),
emoji: '🇵🇹'
}
export const RUSSIAN: TranslateLanguage = {
value: 'Russian',
langCode: 'ru-ru',
label: () => i18n.t('languages.russian'),
emoji: '🇷🇺'
}
export const POLISH: TranslateLanguage = {
value: 'Polish',
langCode: 'pl-pl',
label: () => i18n.t('languages.polish'),
emoji: '🇵🇱'
}
export const ARABIC: TranslateLanguage = {
value: 'Arabic',
langCode: 'ar-ar',
label: () => i18n.t('languages.arabic'),
emoji: '🇸🇦'
}
export const TURKISH: TranslateLanguage = {
value: 'Turkish',
langCode: 'tr-tr',
label: () => i18n.t('languages.turkish'),
emoji: '🇹🇷'
}
export const THAI: TranslateLanguage = {
value: 'Thai',
langCode: 'th-th',
label: () => i18n.t('languages.thai'),
emoji: '🇹🇭'
}
export const VIETNAMESE: TranslateLanguage = {
value: 'Vietnamese',
langCode: 'vi-vn',
label: () => i18n.t('languages.vietnamese'),
emoji: '🇻🇳'
}
export const INDONESIAN: TranslateLanguage = {
value: 'Indonesian',
langCode: 'id-id',
label: () => i18n.t('languages.indonesian'),
emoji: '🇮🇩'
}
export const URDU: TranslateLanguage = {
value: 'Urdu',
langCode: 'ur-pk',
label: () => i18n.t('languages.urdu'),
emoji: '🇵🇰'
}
export const MALAY: TranslateLanguage = {
value: 'Malay',
langCode: 'ms-my',
label: () => i18n.t('languages.malay'),
emoji: '🇲🇾'
}
export const UKRAINIAN: TranslateLanguage = {
value: 'Ukrainian',
langCode: 'uk-ua',
label: () => i18n.t('languages.ukrainian'),
emoji: '🇺🇦'
}

View File

@ -1,10 +1,12 @@
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger'
import { builtinLanguages, UNKNOWN } from '@renderer/config/translate'
import type { TranslateLanguage } from '@renderer/types'
import type { TranslateLanguageCode } from '@renderer/types'
import { type TranslateLanguage } from '@renderer/types'
import { runAsyncFunction } from '@renderer/utils'
import { getTranslateOptions } from '@renderer/utils/translate'
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
const logger = loggerService.withContext('useTranslate')
@ -16,6 +18,7 @@ const logger = loggerService.withContext('useTranslate')
* - getLanguageByLangcode: 通过语言代码获取语言对象
*/
export default function useTranslate() {
const { t } = useTranslation()
const [prompt] = usePreference('feature.translate.model_prompt')
const [translateLanguages, setTranslateLanguages] = useState<TranslateLanguage[]>(builtinLanguages)
const [isLoaded, setIsLoaded] = useState(false)
@ -46,9 +49,53 @@ export default function useTranslate() {
[isLoaded, translateLanguages]
)
const labelMap: Record<string, string> = useMemo(
() => ({
'zh-cn': t('languages.chinese'),
'zh-tw': t('languages.chinese-traditional'),
'ja-jp': t('languages.japanese'),
'ko-kr': t('languages.korean'),
'en-us': t('languages.english'),
'fr-fr': t('languages.french'),
'de-de': t('languages.german'),
'it-it': t('languages.italian'),
'es-es': t('languages.spanish'),
'pt-pt': t('languages.portuguese'),
'ru-ru': t('languages.russian'),
'pl-pl': t('languages.polish'),
'ar-ar': t('languages.arabic'),
'tr-tr': t('languages.turkish'),
'th-th': t('languages.thai'),
'vi-vn': t('languages.vietnamese'),
'id-id': t('languages.indonesian'),
'ur-pk': t('languages.urdu'),
'ms-my': t('languages.malay'),
'uk-ua': t('languages.ukrainian'),
unknown: t('common.unknown')
}),
[t]
)
const getLanguageLabel = useCallback(
(code: TranslateLanguageCode) => {
const label = labelMap[code]
if (label) {
return label
} else if (isLoaded) {
const language = getLanguageByLangcode(code)
return language.value
} else {
return t('common.unknown')
}
},
[getLanguageByLangcode, isLoaded, labelMap, t]
)
return {
prompt,
isLoaded,
translateLanguages,
getLanguageByLangcode
getLanguageByLangcode,
getLanguageLabel
}
}

View File

@ -23,7 +23,7 @@ import store, { useAppDispatch } from '@renderer/store'
import { messageBlocksSelectors, removeOneBlock } from '@renderer/store/messageBlock'
import { selectMessagesForTopic } from '@renderer/store/newMessage'
import { TraceIcon } from '@renderer/trace/pages/Component'
import type { Assistant, Model, Topic, TranslateLanguage } from '@renderer/types'
import type { Assistant, Model, Topic, TranslateLanguage, TranslateLanguageCode } from '@renderer/types'
import { type Message, MessageBlockType } from '@renderer/types/newMessage'
import { captureScrollableAsBlob, captureScrollableAsDataURL, classNames } from '@renderer/utils'
import { copyMessageAsPlainText } from '@renderer/utils/copy'
@ -118,6 +118,7 @@ type MessageMenubarButtonContext = {
softHoverBg: boolean
t: TFunction
translateLanguages: TranslateLanguage[]
getLanguageLabel: (lang: TranslateLanguageCode) => string
}
type MessageMenubarButtonRenderer = (ctx: MessageMenubarButtonContext) => ReactNode | null
@ -142,7 +143,7 @@ const MessageMenubar: FC<Props> = (props) => {
const [isTranslating, setIsTranslating] = useState(false)
// remove confirm for regenerate; tooltip stays simple
const [showDeleteTooltip, setShowDeleteTooltip] = useState(false)
const { translateLanguages } = useTranslate()
const { translateLanguages, getLanguageLabel } = useTranslate()
// const assistantModel = assistant?.model
const {
deleteMessage,
@ -571,7 +572,8 @@ const MessageMenubar: FC<Props> = (props) => {
showDeleteTooltip,
softHoverBg,
t,
translateLanguages
translateLanguages,
getLanguageLabel
}
return (
@ -757,6 +759,7 @@ const buttonRenderers: Record<MessageMenubarButtonId, MessageMenubarButtonRender
translate: ({
isUserMessage,
translateLanguages,
getLanguageLabel,
handleTranslate,
hasTranslationBlocks,
message,
@ -771,7 +774,7 @@ const buttonRenderers: Record<MessageMenubarButtonId, MessageMenubarButtonRender
const items: MenuProps['items'] = [
...translateLanguages.map((item) => ({
label: item.emoji + ' ' + item.label(),
label: item.emoji + ' ' + getLanguageLabel(item.langCode),
key: item.langCode,
onClick: () => handleTranslate(item)
})),

View File

@ -96,7 +96,7 @@ const SettingsTab: FC<Props> = (props) => {
const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0)
const [fontSizeValue, setFontSizeValue] = useState(fontSize)
const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput)
const { translateLanguages } = useTranslate()
const { translateLanguages, getLanguageLabel } = useTranslate()
const { t } = useTranslation()
@ -142,8 +142,12 @@ const SettingsTab: FC<Props> = (props) => {
)
const targetLanguageItems = useMemo<SelectorItem<string>[]>(
() => translateLanguages.map((item) => ({ value: item.langCode, label: item.emoji + ' ' + item.label() })),
[translateLanguages]
() =>
translateLanguages.map((item) => ({
value: item.langCode,
label: item.emoji + ' ' + getLanguageLabel(item.langCode)
})),
[getLanguageLabel, translateLanguages]
)
const sendMessageShortcutItems = useMemo<SelectorItem<SendMessageShortcut>[]>(
@ -727,7 +731,7 @@ const SettingsTab: FC<Props> = (props) => {
selectionMode="single"
selectedKeys={targetLanguage}
onSelectionChange={(value) => setTargetLanguage(value)}
placeholder={UNKNOWN.emoji + ' ' + UNKNOWN.label()}
placeholder={UNKNOWN.emoji + ' ' + getLanguageLabel(UNKNOWN.langCode)}
items={targetLanguageItems}
/>
</SettingRow>

View File

@ -18,7 +18,7 @@ import { SettingRow, SettingRowTitle } from '..'
export const OcrSystemSettings = () => {
const { t } = useTranslation()
// 和翻译自定义语言耦合了应该还ok
const { translateLanguages } = useTranslate()
const { translateLanguages, getLanguageLabel } = useTranslate()
const { provider, updateConfig } = useOcrProvider(BuiltinOcrProviderIds.system)
if (!isOcrSystemProvider(provider)) {
@ -36,9 +36,9 @@ export const OcrSystemSettings = () => {
() =>
translateLanguages.map((lang) => ({
value: lang.langCode,
label: lang.emoji + ' ' + lang.label()
label: lang.emoji + ' ' + getLanguageLabel(lang.langCode)
})),
[translateLanguages]
[getLanguageLabel, translateLanguages]
)
const onChange = useCallback((value: TranslateLanguageCode[]) => {

View File

@ -24,17 +24,17 @@ export const OcrTesseractSettings = () => {
}
const [langs, setLangs] = useState<Partial<Record<TesseractLangCode, boolean>>>(provider.config?.langs ?? {})
const { translateLanguages } = useTranslate()
const { translateLanguages, getLanguageLabel } = useTranslate()
const options = useMemo(
() =>
translateLanguages
.map((lang) => ({
value: TESSERACT_LANG_MAP[lang.langCode],
label: lang.emoji + ' ' + lang.label()
label: lang.emoji + ' ' + getLanguageLabel(lang.langCode)
}))
.filter((option) => option.value),
[translateLanguages]
[getLanguageLabel, translateLanguages]
)
// TODO: type safe objectKeys

View File

@ -6,7 +6,7 @@ import { DynamicVirtualList } from '@renderer/components/VirtualList'
import db from '@renderer/databases'
import useTranslate from '@renderer/hooks/useTranslate'
import { clearHistory, deleteHistory, updateTranslateHistory } from '@renderer/services/TranslateService'
import type { TranslateHistory, TranslateLanguage } from '@renderer/types'
import type { TranslateHistory } from '@renderer/types'
import { Drawer, Empty, Input, Popconfirm } from 'antd'
import dayjs from 'dayjs'
import { useLiveQuery } from 'dexie-react-hooks'
@ -17,14 +17,9 @@ import { useCallback, useDeferredValue, useEffect, useMemo, useState } from 'rea
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
type DisplayedTranslateHistoryItem = TranslateHistory & {
_sourceLanguage: TranslateLanguage
_targetLanguage: TranslateLanguage
}
type TranslateHistoryProps = {
isOpen: boolean
onHistoryItemClick: (history: DisplayedTranslateHistoryItem) => void
onHistoryItemClick: (history: TranslateHistory) => void
onClose: () => void
}
@ -35,39 +30,34 @@ const ITEM_HEIGHT = 160
const TranslateHistoryList: FC<TranslateHistoryProps> = ({ isOpen, onHistoryItemClick, onClose }) => {
const { t } = useTranslation()
const { getLanguageByLangcode } = useTranslate()
const { getLanguageByLangcode, getLanguageLabel } = useTranslate()
const _translateHistory = useLiveQuery(() => db.translate_history.orderBy('createdAt').reverse().toArray(), [])
const [search, setSearch] = useState('')
const [displayedHistory, setDisplayedHistory] = useState<DisplayedTranslateHistoryItem[]>([])
const [displayedHistory, setDisplayedHistory] = useState<TranslateHistory[]>([])
const [showStared, setShowStared] = useState<boolean>(false)
const translateHistory: DisplayedTranslateHistoryItem[] = useMemo(() => {
const translateHistory: TranslateHistory[] = useMemo(() => {
if (!_translateHistory) return []
return _translateHistory.map((item) => ({
...item,
_sourceLanguage: getLanguageByLangcode(item.sourceLanguage),
_targetLanguage: getLanguageByLangcode(item.targetLanguage),
createdAt: dayjs(item.createdAt).format('MM/DD HH:mm')
}))
}, [_translateHistory, getLanguageByLangcode])
}, [_translateHistory])
const searchFilter = useCallback(
(item: DisplayedTranslateHistoryItem) => {
(item: TranslateHistory) => {
if (isEmpty(search)) return true
const content = `${item._sourceLanguage.label()} ${item._targetLanguage.label()} ${item.sourceText} ${item.targetText} ${item.createdAt}`
const content = `${getLanguageLabel(item.sourceLanguage)} ${getLanguageLabel(item.targetLanguage)} ${item.sourceText} ${item.targetText} ${item.createdAt}`
return content.includes(search)
},
[search]
[getLanguageLabel, search]
)
const starFilter = useMemo(
() => (showStared ? (item: DisplayedTranslateHistoryItem) => !!item.star : () => true),
[showStared]
)
const starFilter = useMemo(() => (showStared ? (item: TranslateHistory) => !!item.star : () => true), [showStared])
const finalFilter = useCallback(
(item: DisplayedTranslateHistoryItem) => searchFilter(item) && starFilter(item),
(item: TranslateHistory) => searchFilter(item) && starFilter(item),
[searchFilter, starFilter]
)
@ -179,8 +169,8 @@ const TranslateHistoryList: FC<TranslateHistoryProps> = ({ isOpen, onHistoryItem
<ColFlex className="h-full w-full flex-1 justify-between gap-1">
<Flex className="h-[30px] items-center justify-between">
<Flex className="items-center gap-1.5">
<HistoryListItemLanguage>{item._sourceLanguage.label()} </HistoryListItemLanguage>
<HistoryListItemLanguage>{item._targetLanguage.label()}</HistoryListItemLanguage>
<HistoryListItemLanguage>{getLanguageLabel(item.sourceLanguage)} </HistoryListItemLanguage>
<HistoryListItemLanguage>{getLanguageLabel(item.targetLanguage)}</HistoryListItemLanguage>
</Flex>
{/* tool bar */}
<Flex className="mt-2 items-center justify-end">

View File

@ -58,7 +58,7 @@ const TranslatePage: FC = () => {
// hooks
const { t } = useTranslation()
const { translateModel, setTranslateModel } = useDefaultModel()
const { prompt, getLanguageByLangcode } = useTranslate()
const { prompt, getLanguageByLangcode, getLanguageLabel } = useTranslate()
const [autoCopy] = usePreference('translate.settings.auto_copy')
const { shikiMarkdownIt } = useCodeStyle()
const { onSelectFile, selecting, clearFiles } = useFiles({ extensions: [...imageExts, ...textExts] })
@ -261,17 +261,15 @@ const TranslatePage: FC = () => {
}
// 控制历史记录点击
const onHistoryItemClick = (
history: TranslateHistory & { _sourceLanguage: TranslateLanguage; _targetLanguage: TranslateLanguage }
) => {
const onHistoryItemClick = (history: TranslateHistory) => {
setText(history.sourceText)
setOutput(history.targetText)
if (history._sourceLanguage === UNKNOWN) {
if (history.sourceLanguage === UNKNOWN.langCode) {
setSourceLanguage('auto')
} else {
setSourceLanguage(history._sourceLanguage)
setSourceLanguage(getLanguageByLangcode(history.sourceLanguage))
}
setTargetLanguage(history._targetLanguage)
setTargetLanguage(getLanguageByLangcode(history.targetLanguage))
setHistoryDrawerVisible(false)
}
@ -390,7 +388,7 @@ const TranslatePage: FC = () => {
return (
<Flex className="min-w-40 items-center">
<BidirectionalLanguageDisplay>
{`${bidirectionalPair[0].label()}${bidirectionalPair[1].label()}`}
{`${getLanguageLabel(bidirectional.origin)}${getLanguageLabel(bidirectional.target)}`}
</BidirectionalLanguageDisplay>
</Flex>
)
@ -656,7 +654,7 @@ const TranslatePage: FC = () => {
{
value: 'auto',
label: detectedLanguage
? `${t('translate.detected.language')} (${detectedLanguage.label()})`
? `${t('translate.detected.language')} (${getLanguageLabel(detectedLanguage.langCode)})`
: t('translate.detected.language')
}
]}

View File

@ -485,7 +485,6 @@ export type TranslateLanguageCode = string
export type TranslateLanguage = {
value: string
langCode: TranslateLanguageCode
label: () => string
emoji: string
}

View File

@ -251,7 +251,6 @@ export const getTranslateOptions = async () => {
// 转换为Language类型
const transformedCustomLangs: TranslateLanguage[] = customLanguages.map((item) => ({
value: item.value,
label: () => item.value,
emoji: item.emoji,
langCode: item.langCode
}))