diff --git a/makefile b/makefile index 2fb914fde92..d98ec63fb7d 100644 --- a/makefile +++ b/makefile @@ -1,8 +1,14 @@ FRONTEND_DIR = ./web/default FRONTEND_CLASSIC_DIR = ./web/classic BACKEND_DIR = . +DEV_COMPOSE_FILE = docker-compose.dev.yml +DEV_POSTGRES_SERVICE = postgres +DEV_BACKEND_SERVICE = new-api +DEV_POSTGRES_DB = new-api +DEV_POSTGRES_USER = root +DEV_SQLITE_PATH ?= one-api.db -.PHONY: all build-frontend build-frontend-classic build-all-frontends start-backend dev dev-api dev-web dev-web-classic +.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 all: build-all-frontends start-backend @@ -22,7 +28,11 @@ start-backend: dev-api: @echo "Starting backend services (docker)..." - @docker compose -f docker-compose.dev.yml up -d + @docker compose -f $(DEV_COMPOSE_FILE) up -d + +dev-api-rebuild: + @echo "Rebuilding and starting backend service (docker)..." + @docker compose -f $(DEV_COMPOSE_FILE) up -d --build $(DEV_BACKEND_SERVICE) dev-web: @echo "Starting frontend dev server..." @@ -33,3 +43,27 @@ dev-web-classic: @cd $(FRONTEND_CLASSIC_DIR) && bun install && bun run dev dev: dev-api dev-web + +reset-setup: + @echo "Resetting local setup wizard state..." + @if docker compose -f $(DEV_COMPOSE_FILE) ps --services --status running | grep -qx "$(DEV_POSTGRES_SERVICE)"; then \ + echo "Detected running docker dev PostgreSQL. Removing setup record and root users..."; \ + docker compose -f $(DEV_COMPOSE_FILE) exec -T $(DEV_POSTGRES_SERVICE) \ + psql -U $(DEV_POSTGRES_USER) -d $(DEV_POSTGRES_DB) \ + -c 'DELETE FROM setups;' \ + -c 'DELETE FROM users WHERE role = 100;' \ + -c "DELETE FROM options WHERE key IN ('SelfUseModeEnabled', 'DemoSiteEnabled');"; \ + echo "Restarting docker dev backend so setup status is recalculated..."; \ + docker compose -f $(DEV_COMPOSE_FILE) restart $(DEV_BACKEND_SERVICE); \ + elif db_path="$${SQLITE_PATH:-$(DEV_SQLITE_PATH)}"; db_path="$${db_path%%\?*}"; [ -f "$$db_path" ]; then \ + db_path="$${SQLITE_PATH:-$(DEV_SQLITE_PATH)}"; \ + db_path="$${db_path%%\?*}"; \ + echo "Detected local SQLite database: $$db_path"; \ + sqlite3 "$$db_path" \ + "DELETE FROM setups; DELETE FROM users WHERE role = 100; DELETE FROM options WHERE key IN ('SelfUseModeEnabled', 'DemoSiteEnabled');"; \ + echo "SQLite setup state reset. Restart the local backend process before testing the setup wizard."; \ + else \ + echo "No running docker dev PostgreSQL or local SQLite database found."; \ + echo "Start the dev stack with 'make dev-api', or set SQLITE_PATH/DEV_SQLITE_PATH to your local SQLite database."; \ + exit 1; \ + fi diff --git a/web/default/src/components/language-switcher.tsx b/web/default/src/components/language-switcher.tsx index cbde4e27eff..e7fdcf2fc83 100644 --- a/web/default/src/components/language-switcher.tsx +++ b/web/default/src/components/language-switcher.tsx @@ -17,6 +17,10 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { useCallback } from 'react' +import { + INTERFACE_LANGUAGE_OPTIONS, + normalizeInterfaceLanguage, +} from '@/i18n/languages' import { Languages, Check } from 'lucide-react' import { useTranslation } from 'react-i18next' import { useAuthStore } from '@/stores/auth-store' @@ -30,18 +34,10 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' -const languages = [ - { code: 'en', label: 'English' }, - { code: 'zh', label: '中文' }, - { code: 'fr', label: 'Français' }, - { code: 'ru', label: 'Русский' }, - { code: 'ja', label: '日本語' }, - { code: 'vi', label: 'Tiếng Việt' }, -] - export function LanguageSwitcher() { const { i18n, t } = useTranslation() const user = useAuthStore((s) => s.auth.user) + const currentLanguage = normalizeInterfaceLanguage(i18n.language) const handleChangeLanguage = useCallback( async (code: string) => { @@ -66,7 +62,7 @@ export function LanguageSwitcher() { {t('Change language')} - {languages.map((lang) => ( + {INTERFACE_LANGUAGE_OPTIONS.map((lang) => ( handleChangeLanguage(lang.code)} @@ -74,7 +70,10 @@ export function LanguageSwitcher() { {lang.label} ))} diff --git a/web/default/src/components/layout/components/chat-presets-item.tsx b/web/default/src/components/layout/components/chat-presets-item.tsx index 0a35e49c66f..f82c78b261a 100644 --- a/web/default/src/components/layout/components/chat-presets-item.tsx +++ b/web/default/src/components/layout/components/chat-presets-item.tsx @@ -79,7 +79,9 @@ function ChatMenuItem({ /> } > - {preset.name} + + {preset.name} + ) @@ -95,11 +97,13 @@ function ChatMenuItem({ isActive={false} className='justify-between' > - {preset.name} + + {preset.name} + {loading ? ( - + ) : ( - + )} diff --git a/web/default/src/components/ui/form.tsx b/web/default/src/components/ui/form.tsx index 69a5ec5d029..16f804ed52d 100644 --- a/web/default/src/components/ui/form.tsx +++ b/web/default/src/components/ui/form.tsx @@ -27,6 +27,7 @@ import { type FieldValues, } from 'react-hook-form' import { useRender } from '@base-ui/react/use-render' +import { useTranslation } from 'react-i18next' import { cn } from '@/lib/utils' import { Label } from '@/components/ui/label' @@ -153,12 +154,15 @@ function FormDescription({ className, ...props }: React.ComponentProps<'p'>) { function FormMessage({ className, ...props }: React.ComponentProps<'p'>) { const { error, formMessageId } = useFormField() + const { t } = useTranslation() const body = error ? String(error?.message ?? '') : props.children if (!body) { return null } + const translatedBody = typeof body === 'string' ? t(body) : body + return (

) { className={cn('text-destructive text-sm', className)} {...props} > - {body} + {translatedBody}

) } diff --git a/web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx b/web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx index a183f70d7fb..18ed116796c 100644 --- a/web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx +++ b/web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx @@ -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( diff --git a/web/default/src/features/channels/components/dialogs/multi-key-manage-dialog.tsx b/web/default/src/features/channels/components/dialogs/multi-key-manage-dialog.tsx index 3e95e7e9372..d87b7fa858d 100644 --- a/web/default/src/features/channels/components/dialogs/multi-key-manage-dialog.tsx +++ b/web/default/src/features/channels/components/dialogs/multi-key-manage-dialog.tsx @@ -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) diff --git a/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx b/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx index 899dd42c1fa..cf346563096 100644 --- a/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx +++ b/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx @@ -698,7 +698,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 ?? '' @@ -733,7 +733,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({ diff --git a/web/default/src/features/profile/components/language-preferences-card.tsx b/web/default/src/features/profile/components/language-preferences-card.tsx index 44cb47ebe15..969ee84e7ca 100644 --- a/web/default/src/features/profile/components/language-preferences-card.tsx +++ b/web/default/src/features/profile/components/language-preferences-card.tsx @@ -17,6 +17,10 @@ along with this program. If not, see . 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) {