Update Owliver language and suggestions

This commit is contained in:
2026-10-07 19:15:44 +05:30
parent 8ac4e1ee88
commit 81e5979f8f
156 changed files with 7330 additions and 1755 deletions

View File

@@ -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>

View File

@@ -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>