Update Owliver language and suggestions
This commit is contained in:
@@ -17,7 +17,8 @@ import {
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
||||
import { AssistantPanel, AssistantPanelProvider } from '@/components/ai-assistant';
|
||||
import { LANGUAGES, setLanguage, useLanguage } from '@/components/ai-assistant/language';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LANGUAGES, setLanguage, useLanguage } from '@/lib/i18n/language';
|
||||
import { UiEditingProvider } from '@/components/ui-tree/UiEditingProvider';
|
||||
import { pageKeyForRoute } from '@/lib/skills/registry';
|
||||
import { endAdminSession } from '@/lib/admin/session';
|
||||
@@ -40,18 +41,22 @@ const ADMIN_ROOT = '/admin';
|
||||
* operations console into another product's navigation loses your place rather
|
||||
* than saving you a step. Their code and their own entry points are untouched.
|
||||
*/
|
||||
/* Keys, not labels. A module-level array is built once, so an English string
|
||||
here would be captured at import and never change when the reader switches
|
||||
language. The key is resolved at render instead. */
|
||||
const NAV = [
|
||||
{ label: 'Control Center', path: ADMIN_ROOT },
|
||||
{ label: 'Positions', path: '/admin/positions' },
|
||||
{ label: 'Candidates', path: '/admin/candidates' },
|
||||
{ label: 'Hired History', path: '/admin/hired' },
|
||||
{ label: 'Talent Pool', path: '/admin/talent-pool' },
|
||||
{ label: 'KROW Forge', path: '/admin/university' },
|
||||
{ label: 'Analytics', path: '/admin/analytics' },
|
||||
{ label: 'Activity', path: '/admin/activity' },
|
||||
{ key: 'nav.controlCenter', path: ADMIN_ROOT },
|
||||
{ key: 'nav.positions', path: '/admin/positions' },
|
||||
{ key: 'nav.candidates', path: '/admin/candidates' },
|
||||
{ key: 'nav.hiredHistory', path: '/admin/hired' },
|
||||
{ key: 'nav.talentPool', path: '/admin/talent-pool' },
|
||||
{ key: 'nav.forge', path: '/admin/university' },
|
||||
{ key: 'nav.analytics', path: '/admin/analytics' },
|
||||
{ key: 'nav.activity', path: '/admin/activity' },
|
||||
];
|
||||
|
||||
function GlobalSearch() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [query, setQuery] = useState('');
|
||||
const [focused, setFocused] = useState(false);
|
||||
@@ -88,8 +93,8 @@ function GlobalSearch() {
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={() => setFocused(false)}
|
||||
placeholder="Search"
|
||||
aria-label="Search the platform"
|
||||
placeholder={t('search.placeholder')}
|
||||
aria-label={t('search.aria')}
|
||||
className="h-8 w-full rounded-lg border border-border bg-surface-subtle pl-8 pr-10 text-body-sm text-ink-1
|
||||
placeholder:text-ink-4 outline-none transition-colors
|
||||
focus:border-krow-blue focus:bg-surface focus:ring-2 focus:ring-krow-blue/15"
|
||||
@@ -108,6 +113,7 @@ interface NotificationsMenuProps {
|
||||
}
|
||||
|
||||
function NotificationsMenu({ items }: NotificationsMenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const unread = items.filter((n) => !n.read).length;
|
||||
|
||||
return (
|
||||
@@ -115,7 +121,7 @@ function NotificationsMenu({ items }: NotificationsMenuProps) {
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={unread ? `Notifications, ${unread} unread` : 'Notifications'}
|
||||
aria-label={unread ? t('notifications.withUnread', { count: unread }) : t('notifications.label')}
|
||||
className="relative grid h-8 w-8 place-items-center rounded-lg text-ink-3 transition-colors
|
||||
hover:bg-surface-sunken hover:text-ink-1
|
||||
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-krow-blue/50"
|
||||
@@ -129,7 +135,7 @@ function NotificationsMenu({ items }: NotificationsMenuProps) {
|
||||
|
||||
<DropdownMenuContent align="end" className="w-80 p-0">
|
||||
<div className="flex items-center justify-between border-b border-border px-3 py-2.5">
|
||||
<p className="text-body-sm font-semibold text-ink-1">Notifications</p>
|
||||
<p className="text-body-sm font-semibold text-ink-1">{t('adminLayout.notifications')}</p>
|
||||
{unread > 0 && <Badge variant="soft" size="sm">{unread} new</Badge>}
|
||||
</div>
|
||||
<div className="max-h-80 overflow-y-auto">
|
||||
@@ -163,6 +169,7 @@ function NotificationsMenu({ items }: NotificationsMenuProps) {
|
||||
}
|
||||
|
||||
export default function AdminLayout() {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
@@ -221,7 +228,7 @@ export default function AdminLayout() {
|
||||
<div className="flex h-14 items-center justify-between gap-4 px-4 lg:px-6">
|
||||
<Link
|
||||
to={ADMIN_ROOT}
|
||||
aria-label="KROW Admin — Control Center"
|
||||
aria-label={t('adminLayout.krowAdminControlCenter')}
|
||||
className="flex shrink-0 items-center rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-krow-blue/50"
|
||||
>
|
||||
<img src={KROW_LOGO_URL} alt="KROW" className="h-6 w-auto object-contain" />
|
||||
@@ -232,7 +239,7 @@ export default function AdminLayout() {
|
||||
and a console header should recede — the underline states position
|
||||
using the brand blue without turning the chrome into the loudest
|
||||
thing on the page. */}
|
||||
<nav aria-label="Admin" className="hidden min-w-0 flex-1 items-center gap-0.5 lg:flex">
|
||||
<nav aria-label={t('nav.admin')} className="hidden min-w-0 flex-1 items-center gap-0.5 lg:flex">
|
||||
{NAV.map((item) => (
|
||||
<Link
|
||||
key={item.path}
|
||||
@@ -244,7 +251,7 @@ export default function AdminLayout() {
|
||||
isActive(item.path) ? 'text-krow-blue' : 'text-ink-3 hover:text-ink-1'
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
{t(item.key)}
|
||||
{isActive(item.path) && (
|
||||
<motion.span
|
||||
layoutId="adminNavIndicator"
|
||||
@@ -269,7 +276,7 @@ export default function AdminLayout() {
|
||||
>
|
||||
<Avatar name={user?.full_name || 'Admin'} size="sm" />
|
||||
<ChevronDown className="h-3 w-3 text-ink-4" aria-hidden="true" />
|
||||
<span className="sr-only">Account menu</span>
|
||||
<span className="sr-only">{t('adminLayout.accountMenu')}</span>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
@@ -277,7 +284,7 @@ export default function AdminLayout() {
|
||||
<DropdownMenuLabel className="font-normal">
|
||||
<p className="text-body-sm font-semibold text-ink-1">{user?.full_name || 'Admin'}</p>
|
||||
<p className="text-caption text-ink-3">{user?.email}</p>
|
||||
<Badge variant="soft" size="sm" className="mt-1.5">Platform Administrator</Badge>
|
||||
<Badge variant="soft" size="sm" className="mt-1.5">{t('adminLayout.platformAdministrator')}</Badge>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{/* Account configuration, then the one thing people come to
|
||||
@@ -285,17 +292,17 @@ export default function AdminLayout() {
|
||||
Profile is not dropped — it is the first entry inside
|
||||
Settings, where account information belongs. */}
|
||||
<DropdownMenuItem onClick={() => navigate('/admin/settings')} className="cursor-pointer">
|
||||
<Settings className="mr-2 h-4 w-4" /> Settings
|
||||
<Settings className="mr-2 h-4 w-4" /> {t('account.settings')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => navigate('/admin/profile#security')} className="cursor-pointer">
|
||||
<Shield className="mr-2 h-4 w-4" /> Security
|
||||
<Shield className="mr-2 h-4 w-4" /> {t('account.security')}
|
||||
</DropdownMenuItem>
|
||||
{/* Straight to the registry itself. The Workspace overview at
|
||||
`/admin/workspace` still exists and still reaches the same
|
||||
list through its own Manage Agents button — it is simply
|
||||
not a stop on the way there from this menu. */}
|
||||
<DropdownMenuItem onClick={() => navigate('/admin/workspace/agents')} className="cursor-pointer">
|
||||
<LayoutGrid className="mr-2 h-4 w-4" /> Agent Registry
|
||||
<LayoutGrid className="mr-2 h-4 w-4" /> {t('account.agentRegistry')}
|
||||
</DropdownMenuItem>
|
||||
|
||||
{/* Language.
|
||||
@@ -310,14 +317,15 @@ export default function AdminLayout() {
|
||||
screen reader and to the eye, without the menu having to
|
||||
draw its own tick.
|
||||
|
||||
What it governs is Owliver's replies. Nothing else in the
|
||||
product is generated text, so nothing else changes; the
|
||||
row is in the account menu rather than in the panel
|
||||
It governs the whole interface and Owliver's replies
|
||||
alike — one choice, because a dashboard in Spanish whose
|
||||
assistant answers in English is not a translated product.
|
||||
The row is in the account menu rather than in the panel
|
||||
because it is a property of the account and outlives any
|
||||
one conversation. */}
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger className="cursor-pointer">
|
||||
<Globe className="mr-2 h-4 w-4" /> Language
|
||||
<Globe className="mr-2 h-4 w-4" /> {t('account.language')}
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
@@ -341,7 +349,7 @@ export default function AdminLayout() {
|
||||
onClick={signOut}
|
||||
className="cursor-pointer text-destructive focus:text-destructive"
|
||||
>
|
||||
<LogOut className="mr-2 h-4 w-4" /> Sign out
|
||||
<LogOut className="mr-2 h-4 w-4" /> {t('account.signOut')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -349,7 +357,7 @@ export default function AdminLayout() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMenuOpen(true)}
|
||||
aria-label="Open navigation"
|
||||
aria-label={t('nav.openNavigation')}
|
||||
className="grid h-8 w-8 place-items-center rounded-lg text-ink-3 hover:bg-surface-sunken hover:text-ink-1 lg:hidden
|
||||
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-krow-blue/50"
|
||||
>
|
||||
@@ -363,7 +371,7 @@ export default function AdminLayout() {
|
||||
<Sheet open={menuOpen} onOpenChange={setMenuOpen}>
|
||||
<SheetContent side="right" className="w-72 p-0">
|
||||
<SheetHeader className="border-b border-border px-4 py-4">
|
||||
<SheetTitle className="text-left font-heading text-title">Admin</SheetTitle>
|
||||
<SheetTitle className="text-left font-heading text-title">{t('adminLayout.admin')}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<nav className="flex flex-col p-2">
|
||||
{NAV.map((item) => (
|
||||
@@ -376,22 +384,22 @@ export default function AdminLayout() {
|
||||
isActive(item.path) ? 'bg-krow-blue-tint text-krow-blue' : 'text-ink-2 hover:bg-surface-sunken'
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
{t(item.key)}
|
||||
</Link>
|
||||
))}
|
||||
<div className="my-2 h-px bg-border" />
|
||||
{[
|
||||
{ label: 'Settings', path: '/admin/settings' },
|
||||
{ label: 'Security', path: '/admin/profile#security' },
|
||||
{ label: 'Agent Registry', path: '/admin/workspace/agents' },
|
||||
{ key: 'account.settings', path: '/admin/settings' },
|
||||
{ key: 'account.security', path: '/admin/profile#security' },
|
||||
{ key: 'account.agentRegistry', path: '/admin/workspace/agents' },
|
||||
].map((item) => (
|
||||
<Link
|
||||
key={item.label}
|
||||
key={item.key}
|
||||
to={item.path}
|
||||
onClick={() => setMenuOpen(false)}
|
||||
className="rounded-lg px-3 py-2.5 text-body-sm font-medium text-ink-2 hover:bg-surface-sunken"
|
||||
>
|
||||
{item.label}
|
||||
{t(item.key)}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuIte
|
||||
import OwliverChatBubble from '@/components/krow/OwliverChatBubble';
|
||||
import { HoverCard, HoverCardTrigger, HoverCardContent } from '@/components/ui/hover-card';
|
||||
import KrowIdPreview from '@/components/krow/KrowIdPreview';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const ROLE_NAV = {
|
||||
employer: [
|
||||
@@ -45,6 +46,7 @@ const ROLE_LABEL = { employer: 'Employer', admin: 'Admin', talent: 'Talent' };
|
||||
const STATUS_OPTIONS = ['All Statuses', 'Active', 'Draft', 'Paused', 'Closed'];
|
||||
|
||||
export default function Layout() {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
@@ -206,7 +208,7 @@ export default function Layout() {
|
||||
<HoverCardTrigger asChild>
|
||||
<DropdownMenuItem onClick={() => navigate('/me')} className="cursor-pointer">
|
||||
<UserIcon className="w-4 h-4 mr-2" />
|
||||
My KROW ID
|
||||
{t('layout.myKrowId')}
|
||||
</DropdownMenuItem>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent side="left" align="start" sideOffset={8} className="w-[380px] p-3 bg-white border border-[#E5E7EB] rounded-2xl shadow-xl">
|
||||
@@ -216,16 +218,16 @@ export default function Layout() {
|
||||
) : (
|
||||
<DropdownMenuItem onClick={() => navigate('/me')} className="cursor-pointer">
|
||||
<UserIcon className="w-4 h-4 mr-2" />
|
||||
My KROW ID
|
||||
{t('layout.myKrowId')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem onClick={() => navigate('/profile')} className="cursor-pointer">
|
||||
<UserIcon className="w-4 h-4 mr-2" />
|
||||
Profile
|
||||
{t('layout.profile')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handleLogout} className="cursor-pointer text-red-600 focus:text-red-600">
|
||||
<LogOut className="w-4 h-4 mr-2" />
|
||||
Log out
|
||||
{t('layout.logOut')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -255,7 +257,7 @@ export default function Layout() {
|
||||
<input
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search positions..."
|
||||
placeholder={t('layout.searchPositions')}
|
||||
type="text"
|
||||
className="w-full pl-9 pr-4 h-9 text-[13px] rounded-full border border-white/60 bg-white/70 backdrop-blur-sm focus:bg-white focus:border-[#0838E0] focus:outline-none transition-all"
|
||||
/>
|
||||
@@ -280,7 +282,7 @@ export default function Layout() {
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
{/* The second row leaves room for the label on mobile too */}
|
||||
<span>New Position</span>
|
||||
<span>{t('layout.newPosition')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -291,7 +293,7 @@ export default function Layout() {
|
||||
<Sheet open={menuOpen} onOpenChange={setMenuOpen}>
|
||||
<SheetContent side="right" className="w-72 p-0 glass">
|
||||
<SheetHeader className="px-5 pt-5 pb-3 border-b border-white/40">
|
||||
<SheetTitle className="text-left font-heading">Menu</SheetTitle>
|
||||
<SheetTitle className="text-left font-heading">{t('layout.menu')}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<nav className="flex flex-col gap-1 p-3">
|
||||
{/* Role switcher (mobile) */}
|
||||
@@ -341,7 +343,7 @@ export default function Layout() {
|
||||
isActive('/me') ? 'bg-[#EEF3FE] text-[#0838E0]' : 'text-[#374151] hover:bg-white/60'
|
||||
)}
|
||||
>
|
||||
My KROW ID
|
||||
{t('layout.myKrowId')}
|
||||
</Link>
|
||||
<Link
|
||||
to="/profile"
|
||||
@@ -351,14 +353,14 @@ export default function Layout() {
|
||||
isActive('/profile') ? 'bg-[#EEF3FE] text-[#0838E0]' : 'text-[#374151] hover:bg-white/60'
|
||||
)}
|
||||
>
|
||||
Profile
|
||||
{t('layout.profile')}
|
||||
</Link>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="inline-flex items-center gap-2 px-4 py-2.5 rounded-xl text-[14px] font-medium text-red-600 hover:bg-red-50/80 transition-colors w-full"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
Log out
|
||||
{t('layout.logOut')}
|
||||
</button>
|
||||
{!isTalent && (
|
||||
<button
|
||||
@@ -366,7 +368,7 @@ export default function Layout() {
|
||||
className="mt-2 inline-flex items-center justify-center gap-1.5 bg-[#0838E0] text-white text-[14px] font-semibold px-4 py-2.5 rounded-xl transition-all"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
New Position
|
||||
{t('layout.newPosition')}
|
||||
</button>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
Reference in New Issue
Block a user