Skip to content

Commit f69ceb6

Browse files
authored
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.
1 parent 68830e6 commit f69ceb6

22 files changed

Lines changed: 527 additions & 257 deletions

makefile

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
FRONTEND_DIR = ./web/default
22
FRONTEND_CLASSIC_DIR = ./web/classic
33
BACKEND_DIR = .
4+
DEV_COMPOSE_FILE = docker-compose.dev.yml
5+
DEV_POSTGRES_SERVICE = postgres
6+
DEV_BACKEND_SERVICE = new-api
7+
DEV_POSTGRES_DB = new-api
8+
DEV_POSTGRES_USER = root
9+
DEV_SQLITE_PATH ?= one-api.db
410

5-
.PHONY: all build-frontend build-frontend-classic build-all-frontends start-backend dev dev-api dev-web dev-web-classic
11+
.PHONY: all build-frontend build-frontend-classic build-all-frontends start-backend dev dev-api dev-api-rebuild dev-web dev-web-classic reset-setup
612

713
all: build-all-frontends start-backend
814

@@ -22,7 +28,11 @@ start-backend:
2228

2329
dev-api:
2430
@echo "Starting backend services (docker)..."
25-
@docker compose -f docker-compose.dev.yml up -d
31+
@docker compose -f $(DEV_COMPOSE_FILE) up -d
32+
33+
dev-api-rebuild:
34+
@echo "Rebuilding and starting backend service (docker)..."
35+
@docker compose -f $(DEV_COMPOSE_FILE) up -d --build $(DEV_BACKEND_SERVICE)
2636

2737
dev-web:
2838
@echo "Starting frontend dev server..."
@@ -33,3 +43,27 @@ dev-web-classic:
3343
@cd $(FRONTEND_CLASSIC_DIR) && bun install && bun run dev
3444

3545
dev: dev-api dev-web
46+
47+
reset-setup:
48+
@echo "Resetting local setup wizard state..."
49+
@if docker compose -f $(DEV_COMPOSE_FILE) ps --services --status running | grep -qx "$(DEV_POSTGRES_SERVICE)"; then \
50+
echo "Detected running docker dev PostgreSQL. Removing setup record and root users..."; \
51+
docker compose -f $(DEV_COMPOSE_FILE) exec -T $(DEV_POSTGRES_SERVICE) \
52+
psql -U $(DEV_POSTGRES_USER) -d $(DEV_POSTGRES_DB) \
53+
-c 'DELETE FROM setups;' \
54+
-c 'DELETE FROM users WHERE role = 100;' \
55+
-c "DELETE FROM options WHERE key IN ('SelfUseModeEnabled', 'DemoSiteEnabled');"; \
56+
echo "Restarting docker dev backend so setup status is recalculated..."; \
57+
docker compose -f $(DEV_COMPOSE_FILE) restart $(DEV_BACKEND_SERVICE); \
58+
elif db_path="$${SQLITE_PATH:-$(DEV_SQLITE_PATH)}"; db_path="$${db_path%%\?*}"; [ -f "$$db_path" ]; then \
59+
db_path="$${SQLITE_PATH:-$(DEV_SQLITE_PATH)}"; \
60+
db_path="$${db_path%%\?*}"; \
61+
echo "Detected local SQLite database: $$db_path"; \
62+
sqlite3 "$$db_path" \
63+
"DELETE FROM setups; DELETE FROM users WHERE role = 100; DELETE FROM options WHERE key IN ('SelfUseModeEnabled', 'DemoSiteEnabled');"; \
64+
echo "SQLite setup state reset. Restart the local backend process before testing the setup wizard."; \
65+
else \
66+
echo "No running docker dev PostgreSQL or local SQLite database found."; \
67+
echo "Start the dev stack with 'make dev-api', or set SQLITE_PATH/DEV_SQLITE_PATH to your local SQLite database."; \
68+
exit 1; \
69+
fi

web/default/src/components/language-switcher.tsx

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
1717
For commercial licensing, please contact support@quantumnous.com
1818
*/
1919
import { useCallback } from 'react'
20+
import {
21+
INTERFACE_LANGUAGE_OPTIONS,
22+
normalizeInterfaceLanguage,
23+
} from '@/i18n/languages'
2024
import { Languages, Check } from 'lucide-react'
2125
import { useTranslation } from 'react-i18next'
2226
import { useAuthStore } from '@/stores/auth-store'
@@ -30,18 +34,10 @@ import {
3034
DropdownMenuTrigger,
3135
} from '@/components/ui/dropdown-menu'
3236

33-
const languages = [
34-
{ code: 'en', label: 'English' },
35-
{ code: 'zh', label: '中文' },
36-
{ code: 'fr', label: 'Français' },
37-
{ code: 'ru', label: 'Русский' },
38-
{ code: 'ja', label: '日本語' },
39-
{ code: 'vi', label: 'Tiếng Việt' },
40-
]
41-
4237
export function LanguageSwitcher() {
4338
const { i18n, t } = useTranslation()
4439
const user = useAuthStore((s) => s.auth.user)
40+
const currentLanguage = normalizeInterfaceLanguage(i18n.language)
4541

4642
const handleChangeLanguage = useCallback(
4743
async (code: string) => {
@@ -66,15 +62,18 @@ export function LanguageSwitcher() {
6662
<span className='sr-only'>{t('Change language')}</span>
6763
</DropdownMenuTrigger>
6864
<DropdownMenuContent align='end'>
69-
{languages.map((lang) => (
65+
{INTERFACE_LANGUAGE_OPTIONS.map((lang) => (
7066
<DropdownMenuItem
7167
key={lang.code}
7268
onClick={() => handleChangeLanguage(lang.code)}
7369
>
7470
{lang.label}
7571
<Check
7672
size={14}
77-
className={cn('ms-auto', i18n.language !== lang.code && 'hidden')}
73+
className={cn(
74+
'ms-auto',
75+
currentLanguage !== lang.code && 'hidden'
76+
)}
7877
/>
7978
</DropdownMenuItem>
8079
))}

web/default/src/components/layout/components/chat-presets-item.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@ function ChatMenuItem({
7979
/>
8080
}
8181
>
82-
<span>{preset.name}</span>
82+
<span className='min-w-0 flex-1 truncate whitespace-nowrap'>
83+
{preset.name}
84+
</span>
8385
</SidebarMenuSubButton>
8486
</SidebarMenuSubItem>
8587
)
@@ -95,11 +97,13 @@ function ChatMenuItem({
9597
isActive={false}
9698
className='justify-between'
9799
>
98-
<span>{preset.name}</span>
100+
<span className='min-w-0 flex-1 truncate whitespace-nowrap'>
101+
{preset.name}
102+
</span>
99103
{loading ? (
100-
<Loader2 className='h-4 w-4 animate-spin' />
104+
<Loader2 className='h-4 w-4 shrink-0 animate-spin' />
101105
) : (
102-
<ExternalLink className='h-4 w-4' />
106+
<ExternalLink className='h-4 w-4 shrink-0' />
103107
)}
104108
</SidebarMenuSubButton>
105109
</SidebarMenuSubItem>

web/default/src/components/ui/form.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
type FieldValues,
2828
} from 'react-hook-form'
2929
import { useRender } from '@base-ui/react/use-render'
30+
import { useTranslation } from 'react-i18next'
3031
import { cn } from '@/lib/utils'
3132
import { Label } from '@/components/ui/label'
3233

@@ -153,20 +154,23 @@ function FormDescription({ className, ...props }: React.ComponentProps<'p'>) {
153154

154155
function FormMessage({ className, ...props }: React.ComponentProps<'p'>) {
155156
const { error, formMessageId } = useFormField()
157+
const { t } = useTranslation()
156158
const body = error ? String(error?.message ?? '') : props.children
157159

158160
if (!body) {
159161
return null
160162
}
161163

164+
const translatedBody = typeof body === 'string' ? t(body) : body
165+
162166
return (
163167
<p
164168
data-slot='form-message'
165169
id={formMessageId}
166170
className={cn('text-destructive text-sm', className)}
167171
{...props}
168172
>
169-
{body}
173+
{translatedBody}
170174
</p>
171175
)
172176
}

web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,13 +232,20 @@ export function ChannelTestDialog({
232232
} catch (error: unknown) {
233233
updateTestResult(model, {
234234
status: 'error',
235-
error: error instanceof Error ? error.message : 'Test failed',
235+
error: error instanceof Error ? error.message : t('Test failed'),
236236
})
237237
} finally {
238238
markModelTesting(model, false)
239239
}
240240
},
241-
[currentRow, endpointType, isStreamTest, markModelTesting, updateTestResult]
241+
[
242+
currentRow,
243+
endpointType,
244+
isStreamTest,
245+
markModelTesting,
246+
t,
247+
updateTestResult,
248+
]
242249
)
243250

244251
const handleBatchTest = useCallback(

web/default/src/features/channels/components/dialogs/multi-key-manage-dialog.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ export function MultiKeyManageDialog({
138138
}
139139
} catch (error: unknown) {
140140
toast.error(
141-
error instanceof Error ? error.message : 'Failed to load key status'
141+
error instanceof Error ? error.message : t('Failed to load key status')
142142
)
143143
} finally {
144144
setIsLoading(false)
@@ -181,7 +181,7 @@ export function MultiKeyManageDialog({
181181
}
182182

183183
if (response?.success) {
184-
toast.success(response.message || 'Operation successful')
184+
toast.success(response.message || t('Operation successful'))
185185
queryClient.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
186186

187187
// Reload data - reset to page 1 for bulk actions
@@ -193,10 +193,12 @@ export function MultiKeyManageDialog({
193193
loadKeyStatus(currentPage, pageSize)
194194
}
195195
} else {
196-
toast.error(response?.message || 'Operation failed')
196+
toast.error(response?.message || t('Operation failed'))
197197
}
198198
} catch (error: unknown) {
199-
toast.error(error instanceof Error ? error.message : 'Operation failed')
199+
toast.error(
200+
error instanceof Error ? error.message : t('Operation failed')
201+
)
200202
} finally {
201203
setIsPerformingAction(false)
202204
setConfirmAction(null)

web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -697,7 +697,7 @@ export function ChannelMutateDrawer({
697697
try {
698698
const res = await getChannelKey(channelId)
699699
if (!res.success) {
700-
throw new Error(res.message || 'Failed to fetch channel key')
700+
throw new Error(res.message || t('Failed to fetch channel key'))
701701
}
702702

703703
const keyValue = res.data?.key ?? ''
@@ -732,7 +732,7 @@ export function ChannelMutateDrawer({
732732
try {
733733
const res = await refreshCodexCredential(channelId)
734734
if (!res.success) {
735-
throw new Error(res.message || 'Failed to refresh credential')
735+
throw new Error(res.message || t('Failed to refresh credential'))
736736
}
737737
toast.success(t('Credential refreshed'))
738738
queryClient.invalidateQueries({

web/default/src/features/profile/components/language-preferences-card.tsx

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
1717
For commercial licensing, please contact support@quantumnous.com
1818
*/
1919
import { useEffect, useMemo, useState } from 'react'
20+
import {
21+
INTERFACE_LANGUAGE_OPTIONS,
22+
normalizeInterfaceLanguage,
23+
} from '@/i18n/languages'
2024
import { Languages, Loader2 } from 'lucide-react'
2125
import { useTranslation } from 'react-i18next'
2226
import { toast } from 'sonner'
@@ -34,24 +38,6 @@ import { updateUserLanguage } from '../api'
3438
import { parseUserSettings } from '../lib'
3539
import type { UserProfile } from '../types'
3640

37-
const LANGUAGE_OPTIONS = [
38-
{ value: 'zh', label: '简体中文' },
39-
{ value: 'en', label: 'English' },
40-
{ value: 'fr', label: 'Français' },
41-
{ value: 'ru', label: 'Русский' },
42-
{ value: 'ja', label: '日本語' },
43-
{ value: 'vi', label: 'Tiếng Việt' },
44-
] as const
45-
46-
function normalizeLanguage(value?: string | null): string {
47-
if (!value) return 'en'
48-
const normalized = value.trim().replace(/_/g, '-').toLowerCase()
49-
if (normalized.startsWith('zh')) return 'zh'
50-
return LANGUAGE_OPTIONS.some((lang) => lang.value === normalized)
51-
? normalized
52-
: 'en'
53-
}
54-
5541
type LanguagePreferencesCardProps = {
5642
profile: UserProfile | null
5743
onProfileUpdate: () => void
@@ -64,7 +50,7 @@ export function LanguagePreferencesCard(props: LanguagePreferencesCardProps) {
6450

6551
const savedLanguage = useMemo(() => {
6652
const settings = parseUserSettings(props.profile?.setting)
67-
return normalizeLanguage(settings.language || i18n.language)
53+
return normalizeInterfaceLanguage(settings.language || i18n.language)
6854
}, [props.profile?.setting, i18n.language])
6955

7056
const [currentLanguage, setCurrentLanguage] = useState(savedLanguage)
@@ -75,7 +61,7 @@ export function LanguagePreferencesCard(props: LanguagePreferencesCardProps) {
7561

7662
const handleLanguageChange = async (language: string | null) => {
7763
if (!language) return
78-
const nextLanguage = normalizeLanguage(language)
64+
const nextLanguage = normalizeInterfaceLanguage(language)
7965
if (nextLanguage === currentLanguage) return
8066

8167
const previousLanguage = currentLanguage
@@ -132,8 +118,8 @@ export function LanguagePreferencesCard(props: LanguagePreferencesCardProps) {
132118
<div className='flex items-center gap-2 sm:min-w-48'>
133119
<Select
134120
items={[
135-
...LANGUAGE_OPTIONS.map((language) => ({
136-
value: language.value,
121+
...INTERFACE_LANGUAGE_OPTIONS.map((language) => ({
122+
value: language.code,
137123
label: language.label,
138124
})),
139125
]}
@@ -146,8 +132,8 @@ export function LanguagePreferencesCard(props: LanguagePreferencesCardProps) {
146132
</SelectTrigger>
147133
<SelectContent alignItemWithTrigger={false}>
148134
<SelectGroup>
149-
{LANGUAGE_OPTIONS.map((language) => (
150-
<SelectItem key={language.value} value={language.value}>
135+
{INTERFACE_LANGUAGE_OPTIONS.map((language) => (
136+
<SelectItem key={language.code} value={language.code}>
151137
{language.label}
152138
</SelectItem>
153139
))}

web/default/src/features/system-settings/content/dashboard-section.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ export function DashboardSection({ defaultValues }: DashboardSectionProps) {
151151
items={[
152152
...granularityOptions.map((option) => ({
153153
value: option.value,
154-
label: option.label,
154+
label: t(option.label),
155155
})),
156156
]}
157157
onValueChange={field.onChange}
@@ -167,7 +167,7 @@ export function DashboardSection({ defaultValues }: DashboardSectionProps) {
167167
<SelectGroup>
168168
{granularityOptions.map((option) => (
169169
<SelectItem key={option.value} value={option.value}>
170-
{option.label}
170+
{t(option.label)}
171171
</SelectItem>
172172
))}
173173
</SelectGroup>

web/default/src/i18n/languages.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
Copyright (C) 2023-2026 QuantumNous
3+
4+
This program is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU Affero General Public License as
6+
published by the Free Software Foundation, either version 3 of the
7+
License, or (at your option) any later version.
8+
9+
This program is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU Affero General Public License for more details.
13+
14+
You should have received a copy of the GNU Affero General Public License
15+
along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
17+
For commercial licensing, please contact support@quantumnous.com
18+
*/
19+
20+
export const INTERFACE_LANGUAGE_OPTIONS = [
21+
{ code: 'zh', label: '简体中文' },
22+
{ code: 'en', label: 'English' },
23+
{ code: 'fr', label: 'Français' },
24+
{ code: 'ru', label: 'Русский' },
25+
{ code: 'ja', label: '日本語' },
26+
{ code: 'vi', label: 'Tiếng Việt' },
27+
] as const
28+
29+
export type InterfaceLanguageCode =
30+
(typeof INTERFACE_LANGUAGE_OPTIONS)[number]['code']
31+
32+
export function normalizeInterfaceLanguage(value?: string | null): string {
33+
if (!value) return 'en'
34+
35+
const normalized = value.trim().replace(/_/g, '-').toLowerCase()
36+
if (normalized.startsWith('zh')) return 'zh'
37+
38+
return INTERFACE_LANGUAGE_OPTIONS.some((lang) => lang.code === normalized)
39+
? normalized
40+
: 'en'
41+
}

0 commit comments

Comments
 (0)