diff --git a/bun.lock b/bun.lock index a792c316..a6da890c 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "name": "librechat-admin-panel", "dependencies": { "@clickhouse/click-ui": "0.15.0", + "@fontsource-variable/vazirmatn": "^5.3.0", "@librechat/data-schemas": "^0.0.74", "@radix-ui/react-dialog": "1.3.1", "@tailwindcss/vite": "^4.3.3", @@ -231,6 +232,8 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="], + "@fontsource-variable/vazirmatn": ["@fontsource-variable/vazirmatn@5.3.0", "", {}, "sha512-ZA68W8JHbMdNzzwjYHUWKB2PH22QpxNEnk0ukcXM+XiqYMV6ZWJZAKuNXcrMR2JjuIAwsfAU3KknCXPI6wXUvw=="], + "@h6s/calendar": ["@h6s/calendar@2.2.0", "", { "peerDependencies": { "react": ">= 18", "react-dom": "*", "react-native": "*" }, "optionalPeers": ["react-dom", "react-native"] }, "sha512-o1fb4RlMWDRIOiMEX65vN5qDTiD/OyC++EkJzmoz9l1vUOkN89gTE+wNE83XZBi3hbYqMV3CNhgKf51n/0hcBw=="], "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="], diff --git a/package.json b/package.json index 7f414e16..f1d2c0c0 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ }, "dependencies": { "@clickhouse/click-ui": "0.15.0", + "@fontsource-variable/vazirmatn": "^5.3.0", "@librechat/data-schemas": "^0.0.74", "@radix-ui/react-dialog": "1.3.1", "@tailwindcss/vite": "^4.3.3", diff --git a/src/components/CommandMenu.tsx b/src/components/CommandMenu.tsx index 870f15ff..925b57cb 100644 --- a/src/components/CommandMenu.tsx +++ b/src/components/CommandMenu.tsx @@ -6,8 +6,9 @@ import { VisuallyHidden } from '@radix-ui/react-visually-hidden'; import { Title as DialogTitle, Description as DialogDescription } from '@radix-ui/react-dialog'; import type * as t from '@/types'; import { matchesQuery, searchConfigIndex } from './configuration/search'; -import { useSearchIndex, useLocalize, useReturnFocus } from '@/hooks'; +import { useSearchIndex, useLocalize, useLanguage, useReturnFocus } from '@/hooks'; import { CONFIG_TABS } from './configuration/configMeta'; +import { LANGUAGES } from '@/constants'; import { pageTitle, restoreFocusLater } from '@/utils'; import { CommandDialog } from '@/components/shared'; import { useTheme } from '@/contexts/ThemeContext'; @@ -24,6 +25,7 @@ export function CommandMenu({ open, onOpenChange }: t.CommandMenuProps) { const localize = useLocalize(); const router = useRouter(); const { setTheme } = useTheme(); + const { setLanguage } = useLanguage(); const { items: settings } = useSearchIndex(localize, open); const [search, setSearch] = useState(''); @@ -111,6 +113,14 @@ export function CommandMenu({ open, onOpenChange }: t.CommandMenuProps) { [setTheme, close], ); + const selectLanguage = useCallback( + (value: t.LanguageCode) => { + setLanguage(value); + close(); + }, + [setLanguage, close], + ); + const navigation: t.StaticCommand[] = [ { id: 'nav-home', @@ -190,6 +200,15 @@ export function CommandMenu({ open, onOpenChange }: t.CommandMenuProps) { keywords: ['theme', 'system', 'auto', 'mode', 'appearance'], onSelect: () => selectTheme('system'), }, + ...LANGUAGES.map( + (option): t.StaticCommand => ({ + id: `language-${option.code}`, + icon: 'globe', + label: localize('com_cmdk_set_language', { language: option.nativeName }), + keywords: ['language', 'locale', 'translation', option.code, option.nativeName], + onSelect: () => selectLanguage(option.code), + }), + ), ]; const query = search.trim(); diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 04bbdaf8..1e3afd2b 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -32,7 +32,7 @@ export function Header({ title, onSearchClick, children }: t.HeaderProps) {
)}
-
+
{children}
{onSearchClick && ( diff --git a/src/components/InputOTP.tsx b/src/components/InputOTP.tsx index a1640714..64766cbe 100644 --- a/src/components/InputOTP.tsx +++ b/src/components/InputOTP.tsx @@ -46,10 +46,10 @@ const InputOTPSlot = React.forwardRef< ref={ref} className={clsx( 'relative flex h-12 w-11 items-center justify-center', - 'border-y border-r border-(--cui-color-stroke-default)', + 'border-y border-e border-(--cui-color-stroke-default)', 'bg-(--cui-color-background-default) text-(--cui-color-text-default)', 'text-lg font-medium shadow-sm transition-all', - 'first:rounded-l-lg first:border-l last:rounded-r-lg', + 'first:rounded-s-lg first:border-s last:rounded-e-lg', isActive ? 'z-10 border-(--cui-color-outline) ring-1 ring-(--cui-color-outline)' : 'hover:border-(--cui-color-stroke-intense)', diff --git a/src/components/LanguageSelector.tsx b/src/components/LanguageSelector.tsx new file mode 100644 index 00000000..11748d30 --- /dev/null +++ b/src/components/LanguageSelector.tsx @@ -0,0 +1,24 @@ +import { Icon } from '@clickhouse/click-ui'; +import { useLocalize, useLanguage } from '@/hooks'; +import { LANGUAGES } from '@/constants'; + +/** Switches to the next shipped language, labeled with that language's own name. */ +export function LanguageSelector() { + const localize = useLocalize(); + const { language, setLanguage } = useLanguage(); + const index = LANGUAGES.findIndex((option) => option.code === language); + const next = LANGUAGES[(index + 1) % LANGUAGES.length]; + + return ( + + ); +} diff --git a/src/components/SettingsDialog.tsx b/src/components/SettingsDialog.tsx index 96d02d8b..e9716e84 100644 --- a/src/components/SettingsDialog.tsx +++ b/src/components/SettingsDialog.tsx @@ -1,7 +1,8 @@ import { Dialog } from '@clickhouse/click-ui'; import type * as t from '@/types'; -import { useLocalize, useReturnFocus } from '@/hooks'; +import { useLocalize, useLanguage, useReturnFocus } from '@/hooks'; import { useTheme } from '@/contexts/ThemeContext'; +import { LANGUAGES } from '@/constants'; import { cn } from '@/utils'; const THEME_OPTIONS: t.ThemeOption[] = ['system', 'light', 'dark']; @@ -15,6 +16,7 @@ export function SettingsDialog({ open, onClose }: t.SettingsDialogProps) { const localize = useLocalize(); const returnFocus = useReturnFocus(open); const { theme, setTheme } = useTheme(); + const { language, setLanguage } = useLanguage(); return (
+
+
+ + {localize('com_nav_language')} + + + {localize('com_settings_language_desc')} + +
+
+ {LANGUAGES.map((opt) => ( + + ))} +
+
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index a21b0004..e26d8be1 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -128,7 +128,7 @@ export function Sidebar({ user, collapsed, onToggle }: t.SidebarProps) {