fix: 修复新 UI 语言与文案显示问题 (#4876)
* chore(dev): add local setup state reset target - add a reset-setup make target to clear setup records, root users, and related options. - support both docker dev PostgreSQL and local SQLite development databases. - restart the docker dev backend so setup status is recalculated after reset. * fix(chat): prevent preset menu text overflow - add truncation layout for chat preset names to keep long labels inside the sidebar menu. - prevent loading and external-link icons from shrinking in constrained menu rows. * fix(i18n): translate dashboard granularity options - call t() for granularity option labels in dashboard system settings. - keep localized text consistent between the select trigger and dropdown items. * chore(dev): add backend dev service rebuild target - add a dev-api-rebuild make target to rebuild and start the docker backend service. - reuse DEV_COMPOSE_FILE and DEV_BACKEND_SERVICE variables to avoid repeated compose config literals. * fix(i18n): align interface language option labels - add shared interface language options to keep display names consistent. - reuse the shared options in the header switcher and profile preferences. - normalize language codes so zh-CN and zh_CN resolve to Simplified Chinese. * fix(i18n): add missing frontend translation keys - route channel key prompts, form validation messages, and channel fallback text through i18n. - add missing translations across six locales for channels, rankings, billing, and logs. - update i18n sync reports so literal t() keys are present in the base locale.
This commit is contained in:
+9
-2
@@ -232,13 +232,20 @@ export function ChannelTestDialog({
|
||||
} catch (error: unknown) {
|
||||
updateTestResult(model, {
|
||||
status: 'error',
|
||||
error: error instanceof Error ? error.message : 'Test failed',
|
||||
error: error instanceof Error ? error.message : t('Test failed'),
|
||||
})
|
||||
} finally {
|
||||
markModelTesting(model, false)
|
||||
}
|
||||
},
|
||||
[currentRow, endpointType, isStreamTest, markModelTesting, updateTestResult]
|
||||
[
|
||||
currentRow,
|
||||
endpointType,
|
||||
isStreamTest,
|
||||
markModelTesting,
|
||||
t,
|
||||
updateTestResult,
|
||||
]
|
||||
)
|
||||
|
||||
const handleBatchTest = useCallback(
|
||||
|
||||
+6
-4
@@ -138,7 +138,7 @@ export function MultiKeyManageDialog({
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : 'Failed to load key status'
|
||||
error instanceof Error ? error.message : t('Failed to load key status')
|
||||
)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
@@ -181,7 +181,7 @@ export function MultiKeyManageDialog({
|
||||
}
|
||||
|
||||
if (response?.success) {
|
||||
toast.success(response.message || 'Operation successful')
|
||||
toast.success(response.message || t('Operation successful'))
|
||||
queryClient.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
|
||||
|
||||
// Reload data - reset to page 1 for bulk actions
|
||||
@@ -193,10 +193,12 @@ export function MultiKeyManageDialog({
|
||||
loadKeyStatus(currentPage, pageSize)
|
||||
}
|
||||
} else {
|
||||
toast.error(response?.message || 'Operation failed')
|
||||
toast.error(response?.message || t('Operation failed'))
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
toast.error(error instanceof Error ? error.message : 'Operation failed')
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : t('Operation failed')
|
||||
)
|
||||
} finally {
|
||||
setIsPerformingAction(false)
|
||||
setConfirmAction(null)
|
||||
|
||||
+2
-2
@@ -697,7 +697,7 @@ export function ChannelMutateDrawer({
|
||||
try {
|
||||
const res = await getChannelKey(channelId)
|
||||
if (!res.success) {
|
||||
throw new Error(res.message || 'Failed to fetch channel key')
|
||||
throw new Error(res.message || t('Failed to fetch channel key'))
|
||||
}
|
||||
|
||||
const keyValue = res.data?.key ?? ''
|
||||
@@ -732,7 +732,7 @@ export function ChannelMutateDrawer({
|
||||
try {
|
||||
const res = await refreshCodexCredential(channelId)
|
||||
if (!res.success) {
|
||||
throw new Error(res.message || 'Failed to refresh credential')
|
||||
throw new Error(res.message || t('Failed to refresh credential'))
|
||||
}
|
||||
toast.success(t('Credential refreshed'))
|
||||
queryClient.invalidateQueries({
|
||||
|
||||
+10
-24
@@ -17,6 +17,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
INTERFACE_LANGUAGE_OPTIONS,
|
||||
normalizeInterfaceLanguage,
|
||||
} from '@/i18n/languages'
|
||||
import { Languages, Loader2 } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
@@ -34,24 +38,6 @@ import { updateUserLanguage } from '../api'
|
||||
import { parseUserSettings } from '../lib'
|
||||
import type { UserProfile } from '../types'
|
||||
|
||||
const LANGUAGE_OPTIONS = [
|
||||
{ value: 'zh', label: '简体中文' },
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'fr', label: 'Français' },
|
||||
{ value: 'ru', label: 'Русский' },
|
||||
{ value: 'ja', label: '日本語' },
|
||||
{ value: 'vi', label: 'Tiếng Việt' },
|
||||
] as const
|
||||
|
||||
function normalizeLanguage(value?: string | null): string {
|
||||
if (!value) return 'en'
|
||||
const normalized = value.trim().replace(/_/g, '-').toLowerCase()
|
||||
if (normalized.startsWith('zh')) return 'zh'
|
||||
return LANGUAGE_OPTIONS.some((lang) => lang.value === normalized)
|
||||
? normalized
|
||||
: 'en'
|
||||
}
|
||||
|
||||
type LanguagePreferencesCardProps = {
|
||||
profile: UserProfile | null
|
||||
onProfileUpdate: () => void
|
||||
@@ -64,7 +50,7 @@ export function LanguagePreferencesCard(props: LanguagePreferencesCardProps) {
|
||||
|
||||
const savedLanguage = useMemo(() => {
|
||||
const settings = parseUserSettings(props.profile?.setting)
|
||||
return normalizeLanguage(settings.language || i18n.language)
|
||||
return normalizeInterfaceLanguage(settings.language || i18n.language)
|
||||
}, [props.profile?.setting, i18n.language])
|
||||
|
||||
const [currentLanguage, setCurrentLanguage] = useState(savedLanguage)
|
||||
@@ -75,7 +61,7 @@ export function LanguagePreferencesCard(props: LanguagePreferencesCardProps) {
|
||||
|
||||
const handleLanguageChange = async (language: string | null) => {
|
||||
if (!language) return
|
||||
const nextLanguage = normalizeLanguage(language)
|
||||
const nextLanguage = normalizeInterfaceLanguage(language)
|
||||
if (nextLanguage === currentLanguage) return
|
||||
|
||||
const previousLanguage = currentLanguage
|
||||
@@ -132,8 +118,8 @@ export function LanguagePreferencesCard(props: LanguagePreferencesCardProps) {
|
||||
<div className='flex items-center gap-2 sm:min-w-48'>
|
||||
<Select
|
||||
items={[
|
||||
...LANGUAGE_OPTIONS.map((language) => ({
|
||||
value: language.value,
|
||||
...INTERFACE_LANGUAGE_OPTIONS.map((language) => ({
|
||||
value: language.code,
|
||||
label: language.label,
|
||||
})),
|
||||
]}
|
||||
@@ -146,8 +132,8 @@ export function LanguagePreferencesCard(props: LanguagePreferencesCardProps) {
|
||||
</SelectTrigger>
|
||||
<SelectContent alignItemWithTrigger={false}>
|
||||
<SelectGroup>
|
||||
{LANGUAGE_OPTIONS.map((language) => (
|
||||
<SelectItem key={language.value} value={language.value}>
|
||||
{INTERFACE_LANGUAGE_OPTIONS.map((language) => (
|
||||
<SelectItem key={language.code} value={language.code}>
|
||||
{language.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
|
||||
@@ -151,7 +151,7 @@ export function DashboardSection({ defaultValues }: DashboardSectionProps) {
|
||||
items={[
|
||||
...granularityOptions.map((option) => ({
|
||||
value: option.value,
|
||||
label: option.label,
|
||||
label: t(option.label),
|
||||
})),
|
||||
]}
|
||||
onValueChange={field.onChange}
|
||||
@@ -167,7 +167,7 @@ export function DashboardSection({ defaultValues }: DashboardSectionProps) {
|
||||
<SelectGroup>
|
||||
{granularityOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
{t(option.label)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
|
||||
Reference in New Issue
Block a user