Implements a zero-dependency i18n framework with TypeScript type safety. Thanks to @gy212 for the original i18n infrastructure design (PR #89) and comprehensive translation files (PR #90-96) that inspired this implementation. ## New files (5) - src/i18n/en.ts: English translations (source of truth, ~270 keys), exports TranslationKey type for compile-time key completeness - src/i18n/zh.ts: Chinese translations, typed as Record<TranslationKey, string> - src/i18n/index.ts: translate() function with {param} interpolation, Locale type, SUPPORTED_LOCALES array - src/components/layout/I18nProvider.tsx: React Context provider, loads/persists locale preference via /api/settings/app - src/hooks/useTranslation.ts: useTranslation() hook returning { locale, setLocale, t } ## Infrastructure wiring - src/app/layout.tsx: I18nProvider wrapped inside ThemeProvider - src/app/api/settings/app/route.ts: added "locale" to ALLOWED_KEYS for persistence ## Language picker - src/components/settings/GeneralSection.tsx: added language selection dropdown (Select component) in Settings > General, using SUPPORTED_LOCALES and setLocale() ## String replacements (33 component files) All hardcoded user-facing strings replaced with t() calls: High priority (chat): - ChatListPanel.tsx: session list, search, empty state, relative time - NavRail.tsx: nav labels, theme toggle, auto-approve indicator - MessageInput.tsx: command descriptions, mode labels, attach tooltip - MessageList.tsx: empty state, load earlier, loading indicator - StreamingMessage.tsx: thinking, permission actions - chat/[id]/page.tsx: session title fallback Settings: - GeneralSection.tsx: update card, auto-approve section, warning dialog - SettingsLayout.tsx: title, description, sidebar labels - CliSettingsSection.tsx: form/json tabs, buttons, dynamic field labels (skipDangerousModePermissionPrompt etc.), Enabled/Disabled states - ProviderForm.tsx: dialog title, form labels, buttons - ProviderManager.tsx: status badges, delete dialog, section headers Extensions: - extensions/page.tsx: title, tab labels - SkillsManager.tsx: empty state, loading, search - SkillEditor.tsx: toolbar buttons, placeholder - SkillListItem.tsx: delete confirmation - CreateSkillDialog.tsx: form labels, scope, templates, validation Plugins: - McpManager.tsx: header, tabs, loading state - McpServerList.tsx: empty state, badges, field labels - McpServerEditor.tsx: dialog title, form labels, buttons - ConfigEditor.tsx: save/format buttons Layout: - RightPanel.tsx: panel header, open/close tooltips - FileTree.tsx: search placeholder, empty states - FilePreview.tsx: back button, line count, copy path - DocPreview.tsx: iframe title - ConnectionStatus.tsx: install prompts, version display - InstallWizard.tsx: phase descriptions, button labels - ImportSessionDialog.tsx: search, relative time, import buttons - FolderPicker.tsx: dialog title, loading, buttons - TaskList.tsx: filter tabs, input placeholder, empty states
158 lines
5.8 KiB
TypeScript
158 lines
5.8 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { usePathname, useRouter } from "next/navigation";
|
|
import { useTheme } from "next-themes";
|
|
import { useCallback, useSyncExternalStore } from "react";
|
|
import { HugeiconsIcon } from "@hugeicons/react";
|
|
import {
|
|
Message02Icon,
|
|
GridIcon,
|
|
Settings02Icon,
|
|
Moon02Icon,
|
|
Sun02Icon,
|
|
} from "@hugeicons/core-free-icons";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from "@/components/ui/tooltip";
|
|
import { cn } from "@/lib/utils";
|
|
import { useTranslation } from "@/hooks/useTranslation";
|
|
import type { TranslationKey } from "@/i18n";
|
|
|
|
|
|
interface NavRailProps {
|
|
chatListOpen: boolean;
|
|
onToggleChatList: () => void;
|
|
hasUpdate?: boolean;
|
|
skipPermissionsActive?: boolean;
|
|
}
|
|
|
|
const navItems = [
|
|
{ href: "/chat", label: "Chats", icon: Message02Icon },
|
|
{ href: "/extensions", label: "Extensions", icon: GridIcon },
|
|
{ href: "/settings", label: "Settings", icon: Settings02Icon },
|
|
] as const;
|
|
|
|
export function NavRail({ onToggleChatList, hasUpdate, skipPermissionsActive }: NavRailProps) {
|
|
const pathname = usePathname();
|
|
const router = useRouter();
|
|
const { theme, setTheme } = useTheme();
|
|
const { t } = useTranslation();
|
|
const navLabelKeys: Record<string, TranslationKey> = {
|
|
'Chats': 'nav.chats',
|
|
'Extensions': 'nav.extensions',
|
|
'Settings': 'nav.settings',
|
|
};
|
|
const emptySubscribe = useCallback(() => () => {}, []);
|
|
const mounted = useSyncExternalStore(emptySubscribe, () => true, () => false);
|
|
const isChatRoute = pathname === "/chat" || pathname.startsWith("/chat/");
|
|
|
|
return (
|
|
<aside className="flex w-14 shrink-0 flex-col items-center bg-sidebar pb-3 pt-10">
|
|
{/* Nav icons */}
|
|
<nav className="flex flex-1 flex-col items-center gap-1">
|
|
{navItems.map((item) => {
|
|
const isActive =
|
|
item.href === "/chat"
|
|
? pathname === "/chat" || pathname.startsWith("/chat/")
|
|
: item.href === "/extensions"
|
|
? pathname.startsWith("/extensions")
|
|
: pathname === item.href || pathname.startsWith(item.href + "?");
|
|
|
|
return (
|
|
<Tooltip key={item.href}>
|
|
<TooltipTrigger asChild>
|
|
{item.href === "/chat" ? (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className={cn(
|
|
"h-9 w-9",
|
|
isActive && "bg-sidebar-accent text-sidebar-accent-foreground"
|
|
)}
|
|
onClick={() => {
|
|
if (!isChatRoute) {
|
|
// Navigate to chat page first, then open chat list
|
|
router.push("/chat");
|
|
onToggleChatList();
|
|
} else {
|
|
onToggleChatList();
|
|
}
|
|
}}
|
|
>
|
|
<HugeiconsIcon icon={item.icon} className="h-4 w-4" />
|
|
<span className="sr-only">{t(navLabelKeys[item.label] ?? item.label as TranslationKey)}</span>
|
|
</Button>
|
|
) : (
|
|
<div className="relative">
|
|
<Button
|
|
asChild
|
|
variant="ghost"
|
|
size="icon"
|
|
className={cn(
|
|
"h-9 w-9",
|
|
isActive && "bg-sidebar-accent text-sidebar-accent-foreground"
|
|
)}
|
|
>
|
|
<Link href={item.href}>
|
|
<HugeiconsIcon icon={item.icon} className="h-4 w-4" />
|
|
<span className="sr-only">{t(navLabelKeys[item.label] ?? item.label as TranslationKey)}</span>
|
|
</Link>
|
|
</Button>
|
|
{item.href === "/settings" && hasUpdate && (
|
|
<span className="absolute top-0.5 right-0.5 h-2 w-2 rounded-full bg-blue-500" />
|
|
)}
|
|
</div>
|
|
)}
|
|
</TooltipTrigger>
|
|
<TooltipContent side="right">{t(navLabelKeys[item.label] ?? item.label as TranslationKey)}</TooltipContent>
|
|
</Tooltip>
|
|
);
|
|
})}
|
|
</nav>
|
|
|
|
{/* Bottom: skip-permissions indicator + theme toggle */}
|
|
<div className="mt-auto flex flex-col items-center gap-2">
|
|
{skipPermissionsActive && (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<div className="flex h-8 w-8 items-center justify-center">
|
|
<span className="relative flex h-3 w-3">
|
|
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-orange-400 opacity-75" />
|
|
<span className="relative inline-flex h-3 w-3 rounded-full bg-orange-500" />
|
|
</span>
|
|
</div>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="right">{t('nav.autoApproveOn')}</TooltipContent>
|
|
</Tooltip>
|
|
)}
|
|
{mounted && (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
|
className="h-8 w-8"
|
|
>
|
|
{theme === "dark" ? (
|
|
<HugeiconsIcon icon={Sun02Icon} className="h-4 w-4" />
|
|
) : (
|
|
<HugeiconsIcon icon={Moon02Icon} className="h-4 w-4" />
|
|
)}
|
|
<span className="sr-only">{t('nav.toggleTheme')}</span>
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="right">
|
|
{theme === "dark" ? t('nav.lightMode') : t('nav.darkMode')}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
)}
|
|
</div>
|
|
</aside>
|
|
);
|
|
}
|