Compare commits

...

4 Commits

Author SHA1 Message Date
Dani Akash
fc84af8d14 feat: created translations 2026-03-20 13:21:39 +05:30
Dani Akash
d01eac9d0b feat: update script to use ai sdk 2026-03-20 13:15:33 +05:30
Dani Akash
be2422437c fix: lint errors 2026-03-20 13:10:04 +05:30
Dani Akash
b2d3acde4e feat: setup english language via internationalization 2026-03-20 13:06:54 +05:30
34 changed files with 1580 additions and 115 deletions

View File

@@ -1,6 +1,7 @@
import { ChevronDown, LogIn, LogOut, User } from 'lucide-react'
import type { FC } from 'react'
import { useNavigate } from 'react-router'
import { i18n } from '#i18n'
import ProductLogo from '@/assets/product_logo.svg'
import { ThemeToggle } from '@/components/elements/theme-toggle'
import {
@@ -68,7 +69,11 @@ export const SidebarBranding: FC<SidebarBrandingProps> = ({
</div>
)
) : (
<img src={ProductLogo} alt="BrowserOS" className="size-8" />
<img
src={ProductLogo}
alt={i18n.t('sidebar.branding.alt')}
className="size-8"
/>
)
return (
@@ -105,7 +110,9 @@ export const SidebarBranding: FC<SidebarBrandingProps> = ({
: 'font-medium text-primary',
)}
>
{isLoggedIn ? 'Personal' : 'Sign in'}
{isLoggedIn
? i18n.t('sidebar.branding.personal')
: i18n.t('sidebar.branding.signIn')}
</span>
</div>
</button>
@@ -123,14 +130,14 @@ export const SidebarBranding: FC<SidebarBrandingProps> = ({
{displayName}
</p>
<p className="text-muted-foreground text-xs leading-none">
Personal
{i18n.t('sidebar.branding.personal')}
</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => navigate('/profile')}>
<User className="mr-2 size-4" />
Update Profile
{i18n.t('sidebar.branding.updateProfile')}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
@@ -138,13 +145,13 @@ export const SidebarBranding: FC<SidebarBrandingProps> = ({
variant="destructive"
>
<LogOut className="mr-2 size-4" />
Sign out
{i18n.t('sidebar.branding.signOut')}
</DropdownMenuItem>
</>
) : (
<DropdownMenuItem onClick={() => navigate('/login')}>
<LogIn className="mr-2 size-4" />
Sign in
{i18n.t('sidebar.branding.signIn')}
</DropdownMenuItem>
)}
</DropdownMenuContent>

View File

@@ -9,6 +9,7 @@ import {
} from 'lucide-react'
import type { FC } from 'react'
import { NavLink, useLocation } from 'react-router'
import { i18n } from '#i18n'
import {
Tooltip,
TooltipContent,
@@ -24,40 +25,44 @@ interface SidebarNavigationProps {
}
type NavItem = {
name: string
nameKey: string
to: string
icon: typeof Home
feature?: Feature
}
const primaryNavItems: NavItem[] = [
{ name: 'Home', to: '/home', icon: Home },
{ nameKey: 'sidebar.nav.home', to: '/home', icon: Home },
{
name: 'Connect Apps',
nameKey: 'sidebar.nav.connectApps',
to: '/connect-apps',
icon: PlugZap,
feature: Feature.MANAGED_MCP_SUPPORT,
},
{ name: 'Scheduled Tasks', to: '/scheduled', icon: CalendarClock },
{
name: 'Skills',
nameKey: 'sidebar.nav.scheduledTasks',
to: '/scheduled',
icon: CalendarClock,
},
{
nameKey: 'sidebar.nav.skills',
to: '/home/skills',
icon: Wand2,
feature: Feature.SKILLS_SUPPORT,
},
{
name: 'Memory',
nameKey: 'sidebar.nav.memory',
to: '/home/memory',
icon: Brain,
feature: Feature.MEMORY_SUPPORT,
},
{
name: 'Soul',
nameKey: 'sidebar.nav.soul',
to: '/home/soul',
icon: Sparkles,
feature: Feature.SOUL_SUPPORT,
},
{ name: 'Settings', to: '/settings/ai', icon: Settings },
{ nameKey: 'sidebar.nav.settings', to: '/settings/ai', icon: Settings },
]
export const SidebarNavigation: FC<SidebarNavigationProps> = ({
@@ -97,7 +102,7 @@ export const SidebarNavigation: FC<SidebarNavigationProps> = ({
expanded ? 'opacity-100' : 'opacity-0',
)}
>
{item.name}
{i18n.t(item.nameKey as never)}
</span>
</NavLink>
)
@@ -106,7 +111,9 @@ export const SidebarNavigation: FC<SidebarNavigationProps> = ({
return (
<Tooltip key={item.to}>
<TooltipTrigger asChild>{navItem}</TooltipTrigger>
<TooltipContent side="right">{item.name}</TooltipContent>
<TooltipContent side="right">
{i18n.t(item.nameKey as never)}
</TooltipContent>
</Tooltip>
)
}

View File

@@ -1,5 +1,6 @@
import { Info, Keyboard } from 'lucide-react'
import type { FC } from 'react'
import { i18n } from '#i18n'
import { Button } from '@/components/ui/button'
import {
Tooltip,
@@ -50,7 +51,7 @@ export const SidebarUserFooter: FC<SidebarUserFooterProps> = ({
expanded ? 'opacity-100' : 'opacity-0',
)}
>
About BrowserOS
{i18n.t('sidebar.footer.about')}
</span>
</a>
)
@@ -68,7 +69,7 @@ export const SidebarUserFooter: FC<SidebarUserFooterProps> = ({
expanded ? 'opacity-100' : 'opacity-0',
)}
>
Shortcuts
{i18n.t('sidebar.footer.shortcuts')}
</span>
</Button>
)
@@ -81,7 +82,9 @@ export const SidebarUserFooter: FC<SidebarUserFooterProps> = ({
) : (
<Tooltip>
<TooltipTrigger asChild>{shortcutsButton}</TooltipTrigger>
<TooltipContent side="right">Shortcuts</TooltipContent>
<TooltipContent side="right">
{i18n.t('sidebar.footer.shortcuts')}
</TooltipContent>
</Tooltip>
)}
@@ -90,7 +93,9 @@ export const SidebarUserFooter: FC<SidebarUserFooterProps> = ({
) : (
<Tooltip>
<TooltipTrigger asChild>{aboutLink}</TooltipTrigger>
<TooltipContent side="right">About BrowserOS</TooltipContent>
<TooltipContent side="right">
{i18n.t('sidebar.footer.about')}
</TooltipContent>
</Tooltip>
)}
</div>

View File

@@ -1,6 +1,7 @@
import { Upload, X } from 'lucide-react'
import { AnimatePresence, motion } from 'motion/react'
import { useState } from 'react'
import { i18n } from '#i18n'
import { Button } from '@/components/ui/button'
import {
Card,
@@ -48,7 +49,9 @@ export const ImportDataHint = () => {
<div className="flex items-start justify-between">
<div className="flex items-center gap-2">
<Upload className="size-5 text-muted-foreground" />
<CardTitle className="text-base">Import your data</CardTitle>
<CardTitle className="text-base">
{i18n.t('newtab.importData.title')}
</CardTitle>
</div>
<Button
variant="ghost"
@@ -60,7 +63,7 @@ export const ImportDataHint = () => {
</Button>
</div>
<CardDescription>
Bring bookmarks, history, and passwords from Chrome.
{i18n.t('newtab.importData.description')}
</CardDescription>
<label
htmlFor="import-dont-ask-again"
@@ -73,11 +76,11 @@ export const ImportDataHint = () => {
setDontAskAgain(checked === true)
}
/>
Don't show this again
{i18n.t('newtab.importData.dontShowAgain')}
</label>
<Button className="w-full" onClick={handleImport}>
<Upload className="size-4" />
Open Import Settings
{i18n.t('newtab.importData.openSettings')}
</Button>
</CardHeader>
</Card>

View File

@@ -12,6 +12,7 @@ import {
import { AnimatePresence, motion } from 'motion/react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router'
import { i18n } from '#i18n'
import { ChatProviderSelector } from '@/components/chat/ChatProviderSelector'
import { AppSelector } from '@/components/elements/AppSelector'
import {
@@ -129,7 +130,9 @@ export const NewTab = () => {
query: inputValue,
selectedTabs,
})
const searchPlaceholder = `Ask BrowserOS or search ${providerConfig.name}...`
const searchPlaceholder = i18n.t('newtab.search.placeholder', [
providerConfig.name,
])
const {
isOpen,
@@ -495,7 +498,7 @@ export const NewTab = () => {
{selectedTab.title}
</div>
<div className="text-muted-foreground text-xs">
Tab
{i18n.t('newtab.selectedTab')}
</div>
</div>
<button
@@ -569,7 +572,10 @@ export const NewTab = () => {
)}
>
<Folder className="h-4 w-4" />
<span>{selectedFolder?.name || 'Add workspace'}</span>
<span>
{selectedFolder?.name ||
i18n.t('newtab.addWorkspace')}
</span>
<ChevronDown className="h-3 w-3" />
</Button>
</WorkspaceSelector>
@@ -592,7 +598,7 @@ export const NewTab = () => {
)}
>
<Layers className="h-4 w-4" />
<span>Tabs</span>
<span>{i18n.t('common.tabs')}</span>
</Button>
</TabPickerPopover>
</div>
@@ -602,7 +608,7 @@ export const NewTab = () => {
<div className="ml-auto flex items-center gap-1.5">
{connectedManagedServers.length === 0 && (
<span className="flex items-center gap-1 font-semibold text-[var(--accent-orange)] text-sm">
New!
{i18n.t('newtab.appsNew')}
</span>
)}
{connectedManagedServers.length === 0 ? (
@@ -623,14 +629,13 @@ export const NewTab = () => {
)}
>
<PlugZap className="h-4 w-4" />
<span>Apps</span>
<span>{i18n.t('common.apps')}</span>
<ChevronDown className="h-3 w-3" />
</Button>
</TooltipTrigger>
</AppSelector>
<TooltipContent side="left" className="max-w-56">
Apps directly connected will have more accurate and
faster responses for your queries!
{i18n.t('newtab.appsTooltip')}
</TooltipContent>
</Tooltip>
) : (
@@ -667,7 +672,7 @@ export const NewTab = () => {
+{connectedManagedServers.length - 4}
</span>
)}
<span>Apps</span>
<span>{i18n.t('common.apps')}</span>
<ChevronDown className="h-3 w-3" />
</Button>
</AppSelector>

View File

@@ -1,5 +1,6 @@
import { motion } from 'motion/react'
import type { FC } from 'react'
import { i18n } from '#i18n'
import ProductLogoSvg from '@/assets/product_logo.svg'
export const NewTabBranding: FC = () => {
@@ -15,7 +16,11 @@ export const NewTabBranding: FC = () => {
}}
className="flex h-20 w-20 items-center justify-center rounded-xl bg-transparent"
>
<img src={ProductLogoSvg} alt="BrowserOS" className="h-20 w-20" />
<img
src={ProductLogoSvg}
alt={i18n.t('newtab.branding.alt')}
className="h-20 w-20"
/>
</motion.div>
</div>
</div>

View File

@@ -1,6 +1,7 @@
import { ChevronRight, Lightbulb, X } from 'lucide-react'
import { AnimatePresence, motion } from 'motion/react'
import { type FC, useState } from 'react'
import { i18n } from '#i18n'
import { NEWTAB_TIP_DISMISSED_EVENT } from '@/lib/constants/analyticsEvents'
import { track } from '@/lib/metrics/track'
import { dismissTip, shouldShowTip, TIPS } from './tips'
@@ -39,7 +40,7 @@ export const NewTabTip: FC = () => {
<Lightbulb className="h-3.5 w-3.5 flex-shrink-0 text-[var(--accent-orange)]" />
<p className="text-muted-foreground text-xs leading-relaxed">
<span className="font-semibold text-[var(--accent-orange)]">
Tip:
{i18n.t('newtab.tip.label')}
</span>{' '}
{tip.text}
</p>
@@ -47,7 +48,7 @@ export const NewTabTip: FC = () => {
type="button"
onClick={handleNext}
className="flex-shrink-0 rounded-sm p-0.5 text-muted-foreground/50 opacity-0 transition-all hover:text-muted-foreground group-hover:opacity-100"
title="Next tip"
title={i18n.t('newtab.tip.nextTip')}
>
<ChevronRight className="h-3 w-3" />
</button>
@@ -55,7 +56,7 @@ export const NewTabTip: FC = () => {
type="button"
onClick={handleDismiss}
className="flex-shrink-0 rounded-sm p-0.5 text-muted-foreground/50 opacity-0 transition-all hover:text-muted-foreground group-hover:opacity-100"
title="Dismiss"
title={i18n.t('newtab.tip.dismiss')}
>
<X className="h-3 w-3" />
</button>

View File

@@ -1,3 +1,4 @@
import { i18n } from '#i18n'
import {
Dialog,
DialogContent,
@@ -25,10 +26,10 @@ export const ShortcutsDialog = ({
<DialogContent className="styled-scrollbar max-h-[80vh] max-w-xl overflow-y-auto">
<DialogHeader>
<DialogTitle className="font-semibold text-2xl">
Keyboard Shortcuts
{i18n.t('newtab.shortcuts.title')}
</DialogTitle>
<DialogDescription>
Use these shortcuts to navigate BrowserOS faster
{i18n.t('newtab.shortcuts.description')}
</DialogDescription>
</DialogHeader>
@@ -59,7 +60,7 @@ export const ShortcutsDialog = ({
</div>
<div className="mt-8 border-border/50 border-t pt-4 text-center text-muted-foreground text-xs">
More shortcuts coming soon
{i18n.t('newtab.shortcuts.moreComingSoon')}
</div>
</DialogContent>
</Dialog>

View File

@@ -2,6 +2,7 @@ import { Cloud, X } from 'lucide-react'
import { AnimatePresence, motion } from 'motion/react'
import { useState } from 'react'
import { useNavigate } from 'react-router'
import { i18n } from '#i18n'
import { Button } from '@/components/ui/button'
import {
Card,
@@ -47,7 +48,9 @@ export const SignInHint = () => {
<div className="flex items-start justify-between">
<div className="flex items-center gap-2">
<Cloud className="size-5 text-muted-foreground" />
<CardTitle className="text-base">Sync your data</CardTitle>
<CardTitle className="text-base">
{i18n.t('newtab.signIn.title')}
</CardTitle>
</div>
<Button
variant="ghost"
@@ -59,7 +62,7 @@ export const SignInHint = () => {
</Button>
</div>
<CardDescription>
Sign in to sync conversation history to the cloud.
{i18n.t('newtab.signIn.description')}
</CardDescription>
<label
htmlFor="sync-dont-ask-again"
@@ -72,10 +75,10 @@ export const SignInHint = () => {
setDontAskAgain(checked === true)
}
/>
Don't ask again
{i18n.t('newtab.signIn.dontAskAgain')}
</label>
<Button className="w-full" onClick={() => navigate('/login')}>
Sign in
{i18n.t('newtab.signIn.signInButton')}
</Button>
</CardHeader>
</Card>

View File

@@ -1,5 +1,6 @@
import { Globe, X } from 'lucide-react'
import type { FC } from 'react'
import { i18n } from '#i18n'
interface ChatAttachedTabsProps {
tabs: chrome.tabs.Tab[]
@@ -34,7 +35,7 @@ export const ChatAttachedTabs: FC<ChatAttachedTabsProps> = ({
type="button"
onClick={() => onRemoveTab(tab.id)}
className="flex-shrink-0 rounded p-0.5 transition-colors hover:bg-background"
title="Remove tab"
title={i18n.t('attachedTabs.removeTab')}
>
<X className="h-3 w-3 text-muted-foreground" />
</button>

View File

@@ -1,5 +1,6 @@
import { Sparkles } from 'lucide-react'
import type { FC } from 'react'
import { i18n } from '#i18n'
import { cn } from '@/lib/utils'
import { AGENT_SUGGESTIONS, CHAT_SUGGESTIONS, type ChatMode } from './chatTypes'
@@ -28,12 +29,14 @@ export const ChatEmptyState: FC<ChatEmptyStateProps> = ({
</div>
<div>
<h2 className="mb-1 font-semibold text-lg">
{mode === 'chat' ? 'Chat with this page' : 'Agent at your service'}
{mode === 'chat'
? i18n.t('chat.empty.chatTitle')
: i18n.t('chat.empty.agentTitle')}
</h2>
<p className="max-w-[200px] text-muted-foreground text-xs">
{mode === 'chat'
? 'Ask questions about the current page or any topic'
: 'Let AI automate tasks and browse for you'}
? i18n.t('chat.empty.chatSubtitle')
: i18n.t('chat.empty.agentSubtitle')}
</p>
</div>

View File

@@ -1,5 +1,6 @@
import { AlertCircle, RefreshCw } from 'lucide-react'
import type { FC } from 'react'
import { i18n } from '#i18n'
// import { useMemo } from 'react'
import { Button } from '@/components/ui/button'
import {
@@ -38,7 +39,7 @@ function parseErrorMessage(message: string): {
message.includes('127.0.0.1')
) {
return {
text: 'Unable to connect to BrowserOS agent. Follow below instructions.',
text: i18n.t('chat.error.connectionMessage'),
url: 'https://docs.browseros.com/troubleshooting/connection-issues',
isConnectionError: true,
}
@@ -47,7 +48,7 @@ function parseErrorMessage(message: string): {
// Detect BrowserOS rate limit (unique pattern, no provider uses this)
if (message.includes('BrowserOS LLM daily limit reached')) {
return {
text: 'Add your own API key for unlimited usage.',
text: i18n.t('chat.error.rateLimitMessage'),
url: 'https://dub.sh/browseros-usage-limit',
isRateLimit: true,
}
@@ -83,9 +84,9 @@ export const ChatError: FC<ChatErrorProps> = ({ error, onRetry }) => {
// --- End commented out survey code ---
const getTitle = () => {
if (isRateLimit) return 'Daily limit reached'
if (isConnectionError) return 'Connection failed'
return 'Something went wrong'
if (isRateLimit) return i18n.t('chat.error.dailyLimitTitle')
if (isConnectionError) return i18n.t('chat.error.connectionFailedTitle')
return i18n.t('chat.error.genericTitle')
}
return (
@@ -102,7 +103,7 @@ export const ChatError: FC<ChatErrorProps> = ({ error, onRetry }) => {
rel="noopener noreferrer"
className="text-muted-foreground text-xs underline hover:text-foreground"
>
View troubleshooting guide
{i18n.t('chat.error.troubleshootingLink')}
</a>
)}
{/* --- Commented out for Kimi partnership launch (restore after) ---
@@ -139,7 +140,7 @@ export const ChatError: FC<ChatErrorProps> = ({ error, onRetry }) => {
className="underline hover:text-foreground"
onClick={() => track(KIMI_RATE_LIMIT_DOCS_CLICKED_EVENT)}
>
Learn how to get a Kimi API key
{i18n.t('chat.error.kimiApiKeyLink')}
</a>
{' or '}
<a
@@ -149,7 +150,7 @@ export const ChatError: FC<ChatErrorProps> = ({ error, onRetry }) => {
className="underline hover:text-foreground"
onClick={() => track(KIMI_RATE_LIMIT_PLATFORM_CLICKED_EVENT)}
>
get your API key
{i18n.t('chat.error.getApiKey')}
</a>
</p>
</div>
@@ -162,7 +163,7 @@ export const ChatError: FC<ChatErrorProps> = ({ error, onRetry }) => {
className="mt-1 gap-2"
>
<RefreshCw className="h-3.5 w-3.5" />
Try again
{i18n.t('chat.error.tryAgain')}
</Button>
)}
</div>

View File

@@ -1,6 +1,7 @@
import { ChevronDown, Folder, Layers, PlugZap } from 'lucide-react'
import type { FC, FormEvent } from 'react'
import { useEffect, useRef, useState } from 'react'
import { i18n } from '#i18n'
import { AppSelector } from '@/components/elements/AppSelector'
import { WorkspaceSelector } from '@/components/elements/workspace-selector'
import { McpServerIcon } from '@/entrypoints/app/connect-mcp/McpServerIcon'
@@ -142,7 +143,7 @@ export const ChatFooter: FC<ChatFooterProps> = ({
aria-expanded={isTabMentionOpen}
aria-haspopup="dialog"
className="flex cursor-pointer items-center gap-1 rounded-lg p-1.5 text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground data-[state=open]:bg-accent"
title="Attach tabs (@)"
title={i18n.t('footer.attachTabs')}
>
<Layers className="h-4 w-4" />
{attachedTabs.length > 0 && (
@@ -166,7 +167,7 @@ export const ChatFooter: FC<ChatFooterProps> = ({
title={
selectedFolder
? selectedFolder.name
: 'Select workspace folder'
: i18n.t('footer.selectWorkspace')
}
>
<div className="relative">
@@ -185,7 +186,7 @@ export const ChatFooter: FC<ChatFooterProps> = ({
<button
type="button"
className="flex cursor-pointer items-center gap-1 rounded-lg p-1.5 text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground data-[state=open]:bg-accent"
title="Connect apps"
title={i18n.t('footer.connectApps')}
>
{connectedManagedServers.length > 0 ? (
<>

View File

@@ -1,6 +1,7 @@
import { Github, History, Plus, SettingsIcon } from 'lucide-react'
import type { FC } from 'react'
import { Link, useLocation, useNavigate } from 'react-router'
import { i18n } from '#i18n'
import { ChatProviderSelector } from '@/components/chat/ChatProviderSelector'
import type { Provider } from '@/components/chat/chatComponentTypes'
import { ThemeToggle } from '@/components/elements/theme-toggle'
@@ -46,7 +47,7 @@ export const ChatHeader: FC<ChatHeaderProps> = ({
<button
type="button"
className="group relative inline-flex cursor-pointer items-center gap-2 rounded-lg p-2 text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground data-[state=open]:bg-accent"
title="Change AI Provider"
title={i18n.t('chat.header.changeProvider')}
>
{selectedProvider.type === 'browseros' ? (
<BrowserOSIcon size={18} />
@@ -69,7 +70,7 @@ export const ChatHeader: FC<ChatHeaderProps> = ({
type="button"
onClick={onNewConversation}
className="cursor-pointer rounded-lg p-2 text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground"
title="New conversation"
title={i18n.t('chat.header.newConversation')}
>
<Plus className="h-4 w-4" />
</button>
@@ -81,7 +82,7 @@ export const ChatHeader: FC<ChatHeaderProps> = ({
type="button"
onClick={handleNewConversationFromHistory}
className="cursor-pointer rounded-lg p-2 text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground"
title="New conversation"
title={i18n.t('chat.header.newConversation')}
>
<Plus className="h-4 w-4" />
</button>
@@ -89,7 +90,7 @@ export const ChatHeader: FC<ChatHeaderProps> = ({
<Link
to="/history"
className="cursor-pointer rounded-lg p-2 text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground"
title="Chat history"
title={i18n.t('chat.header.chatHistory')}
>
<History className="h-4 w-4" />
</Link>
@@ -100,7 +101,7 @@ export const ChatHeader: FC<ChatHeaderProps> = ({
target="_blank"
rel="noopener noreferrer"
className="cursor-pointer rounded-lg p-2 text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground"
title="Star on Github"
title={i18n.t('chat.header.starOnGithub')}
>
<Github className="h-4 w-4" />
</a>
@@ -110,7 +111,7 @@ export const ChatHeader: FC<ChatHeaderProps> = ({
target="_blank"
rel="noopener noreferrer"
className="cursor-pointer rounded-lg p-2 text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground"
title="Settings"
title={i18n.t('chat.header.settings')}
>
<SettingsIcon className="h-4 w-4" />
</a>

View File

@@ -8,6 +8,7 @@ import {
useRef,
useState,
} from 'react'
import { i18n } from '#i18n'
import { TabPickerPopover } from '@/components/elements/tab-picker-popover'
import { cn } from '@/lib/utils'
import type { VoiceInputState } from '@/lib/voice/useVoiceInput'
@@ -273,7 +274,9 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
className="cursor-pointer rounded-full bg-red-600 p-2 text-white shadow-sm transition-all duration-200 hover:bg-red-900"
>
<Square className="h-3.5 w-3.5" />
<span className="sr-only">Stop recording</span>
<span className="sr-only">
{i18n.t('chat.input.stopRecording')}
</span>
</button>
)
}
@@ -286,7 +289,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
className="rounded-full p-2 text-muted-foreground"
>
<Loader2 className="h-3.5 w-3.5 animate-spin" />
<span className="sr-only">Transcribing</span>
<span className="sr-only">{i18n.t('chat.input.transcribing')}</span>
</button>
)
}
@@ -299,7 +302,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
className="cursor-pointer rounded-full p-2 text-muted-foreground transition-all duration-200 hover:bg-muted hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
>
<Mic className="h-3.5 w-3.5" />
<span className="sr-only">Voice input</span>
<span className="sr-only">{i18n.t('chat.input.voiceInput')}</span>
</button>
)
}
@@ -313,7 +316,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
className="cursor-pointer rounded-full bg-red-600 p-2 text-white shadow-sm transition-all duration-200 hover:bg-red-900"
>
<SquareStop className="h-3.5 w-3.5" />
<span className="sr-only">Stop</span>
<span className="sr-only">{i18n.t('chat.input.stop')}</span>
</button>
)
}
@@ -327,7 +330,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
className="cursor-pointer rounded-full bg-[var(--accent-orange)] p-2 text-white shadow-sm transition-all duration-200 hover:bg-[var(--accent-orange-bright)] disabled:cursor-not-allowed disabled:opacity-50"
>
<Send className="h-3.5 w-3.5" />
<span className="sr-only">Send</span>
<span className="sr-only">{i18n.t('chat.input.send')}</span>
</button>
)
}
@@ -370,10 +373,10 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
onKeyDown={handleKeyDown}
placeholder={
voice?.isTranscribing
? 'Transcribing...'
? i18n.t('chat.input.placeholderTranscribing')
: mode === 'chat'
? 'Ask about this page...'
: 'What should I do?'
? i18n.t('chat.input.placeholderChat')
: i18n.t('chat.input.placeholderAgent')
}
disabled={voice?.isTranscribing}
rows={1}

View File

@@ -1,6 +1,7 @@
import { CheckIcon, CopyIcon, ThumbsDownIcon, ThumbsUpIcon } from 'lucide-react'
import { AnimatePresence, motion } from 'motion/react'
import { type FC, useState } from 'react'
import { i18n } from '#i18n'
import { MessageAction, MessageActions } from '@/components/ai-elements/message'
import { Button } from '@/components/ui/button'
import {
@@ -63,8 +64,8 @@ export const ChatMessageActions: FC<ChatMessageActionsProps> = ({
navigator.clipboard.writeText(messageText)
track(SIDEPANEL_MESSAGE_COPIED_EVENT)
}}
label="Copy"
tooltip="Copy to clipboard"
label={i18n.t('chat.actions.copy')}
tooltip={i18n.t('chat.actions.copyToClipboard')}
>
<CopyIcon className="size-3" />
</MessageAction>
@@ -79,7 +80,7 @@ export const ChatMessageActions: FC<ChatMessageActionsProps> = ({
className="flex items-center gap-1 text-muted-foreground text-xs"
>
<CheckIcon className="size-3" />
<span>Feedback submitted</span>
<span>{i18n.t('chat.actions.feedbackSubmitted')}</span>
</motion.div>
) : (
<motion.div
@@ -91,9 +92,9 @@ export const ChatMessageActions: FC<ChatMessageActionsProps> = ({
className="flex items-center gap-1"
>
<MessageAction
label="Like"
label={i18n.t('chat.actions.like')}
onClick={handleLike}
tooltip="Like this response"
tooltip={i18n.t('chat.actions.likeTooltip')}
>
<ThumbsUpIcon
className="size-4"
@@ -101,9 +102,9 @@ export const ChatMessageActions: FC<ChatMessageActionsProps> = ({
/>
</MessageAction>
<MessageAction
label="Dislike"
label={i18n.t('chat.actions.dislike')}
onClick={handleDislikeClick}
tooltip="Dislike this response"
tooltip={i18n.t('chat.actions.dislikeTooltip')}
>
<ThumbsDownIcon
className="size-4"
@@ -117,13 +118,13 @@ export const ChatMessageActions: FC<ChatMessageActionsProps> = ({
<Dialog open={dislikeDialogOpen} onOpenChange={setDislikeDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>What went wrong?</DialogTitle>
<DialogTitle>{i18n.t('chat.actions.feedbackTitle')}</DialogTitle>
<DialogDescription>
Help us improve by sharing what was wrong with this response.
{i18n.t('chat.actions.feedbackDescription')}
</DialogDescription>
</DialogHeader>
<Input
placeholder="Add a comment (optional)"
placeholder={i18n.t('chat.actions.feedbackPlaceholder')}
value={dislikeComment}
onChange={(e) => setDislikeComment(e.target.value)}
onKeyDown={(e) => {
@@ -134,9 +135,11 @@ export const ChatMessageActions: FC<ChatMessageActionsProps> = ({
/>
<DialogFooter>
<Button variant="outline" onClick={handleDislikeCancel}>
Cancel
{i18n.t('common.cancel')}
</Button>
<Button onClick={handleDislikeSubmit}>
{i18n.t('common.submit')}
</Button>
<Button onClick={handleDislikeSubmit}>Submit</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@@ -1,5 +1,6 @@
import { MessageSquare, MousePointer2 } from 'lucide-react'
import type { FC } from 'react'
import { i18n } from '#i18n'
import {
Tooltip,
TooltipContent,
@@ -37,20 +38,20 @@ export const ChatModeToggle: FC<ChatModeToggleProps> = ({
{isAgentMode ? (
<>
<MousePointer2 className="h-3 w-3" />
<span>Agent Mode ON</span>
<span>{i18n.t('chat.mode.agent')}</span>
</>
) : (
<>
<MessageSquare className="h-3 w-3" />
<span>Chat Mode ON</span>
<span>{i18n.t('chat.mode.chat')}</span>
</>
)}
</button>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-[220px]">
{isAgentMode
? 'AI can browse, click, and navigate'
: 'AI can only read, cannot click or navigate'}
? i18n.t('chat.mode.agentTooltip')
: i18n.t('chat.mode.chatTooltip')}
</TooltipContent>
</Tooltip>
</TooltipProvider>

View File

@@ -1,5 +1,6 @@
import { FileText, X } from 'lucide-react'
import type { FC } from 'react'
import { i18n } from '#i18n'
import type { SelectedTextData } from '@/lib/selected-text/selectedTextStorage'
const MAX_DISPLAY_LENGTH = 200
@@ -35,7 +36,7 @@ export const ChatSelectedText: FC<ChatSelectedTextProps> = ({
type="button"
onClick={onDismiss}
className="flex-shrink-0 rounded p-0.5 transition-colors hover:bg-background"
title="Remove selected text"
title={i18n.t('selectedText.removeTitle')}
>
<X className="h-3 w-3 text-muted-foreground" />
</button>

View File

@@ -1,6 +1,7 @@
import { Check, Plug } from 'lucide-react'
import { type FC, useEffect, useState } from 'react'
import { toast } from 'sonner'
import { i18n } from '#i18n'
import { Button } from '@/components/ui/button'
import {
BREADCRUMB_CONNECT_CLICKED_EVENT,
@@ -38,7 +39,11 @@ export const ConnectAppCard: FC<ConnectAppCardProps> = ({
apiKeyUrl: string
} | null>(null)
const [resolvedText, setResolvedText] = useState(
isLastMessage ? '' : `${(data.appName as string) ?? 'App'} suggested`,
isLastMessage
? ''
: i18n.t('chat.connectApp.suggested', [
(data.appName as string) ?? 'App',
]),
)
const { sendMessage } = useChatSessionContext()
@@ -110,9 +115,11 @@ export const ConnectAppCard: FC<ConnectAppCardProps> = ({
managedServerDescription: '',
})
track(MANAGED_MCP_ADDED_EVENT, { server_name: appName })
toast.success(`${apiKeyServer.name} connected successfully`)
toast.success(
i18n.t('chat.connectApp.connectedSuccess', [apiKeyServer.name]),
)
setApiKeyServer(null)
setResolvedText(`Connected ${appName}`)
setResolvedText(i18n.t('chat.connectApp.connected', [appName]))
setPhase('resolved')
sendMessage({
text: `I've connected ${appName}, continue with the task`,
@@ -127,7 +134,7 @@ export const ConnectAppCard: FC<ConnectAppCardProps> = ({
const handleOAuthComplete = () => {
track(BREADCRUMB_CONNECT_COMPLETED_EVENT, { app_name: appName })
setResolvedText(`Connected ${appName}`)
setResolvedText(i18n.t('chat.connectApp.connected', [appName]))
setPhase('resolved')
sendMessage({
text: `I've connected ${appName}, continue with the task`,
@@ -140,7 +147,7 @@ export const ConnectAppCard: FC<ConnectAppCardProps> = ({
if (!current.includes(appName)) {
await declinedAppsStorage.setValue([...current, appName])
}
setResolvedText(`Continuing without ${appName}`)
setResolvedText(i18n.t('chat.connectApp.continuedWithout', [appName]))
setPhase('resolved')
sendMessage({
text: `Continue without connecting ${appName}, do it manually with browser automation`,
@@ -165,20 +172,20 @@ export const ConnectAppCard: FC<ConnectAppCardProps> = ({
<Plug className="h-5 w-5 shrink-0 text-[var(--accent-orange)]" />
<div>
<p className="font-medium text-sm">
Authorize {appName} in the opened tab
{i18n.t('chat.connectApp.authorizeTitle', [appName])}
</p>
<p className="mt-1 text-muted-foreground text-xs">
Complete the sign-in flow, then click the button below.
{i18n.t('chat.connectApp.authorizeDescription')}
</p>
</div>
</div>
<div className="mt-3 flex gap-2">
<Button size="sm" onClick={handleOAuthComplete}>
I've authorized {appName}, continue
{i18n.t('chat.connectApp.authorizedButton', [appName])}
</Button>
<Button size="sm" variant="ghost" onClick={handleManual}>
Skip, do it manually
{i18n.t('chat.connectApp.skipButton')}
</Button>
</div>
</div>
@@ -192,7 +199,7 @@ export const ConnectAppCard: FC<ConnectAppCardProps> = ({
<Plug className="h-5 w-5 shrink-0 text-[var(--accent-orange)]" />
<div>
<p className="font-medium text-sm">
Connect {appName} for better results
{i18n.t('chat.connectApp.connectForBetter', [appName])}
</p>
{reason && (
<p className="mt-1 text-muted-foreground text-xs">{reason}</p>
@@ -202,10 +209,12 @@ export const ConnectAppCard: FC<ConnectAppCardProps> = ({
<div className="mt-3 flex gap-2">
<Button size="sm" onClick={handleConnect} disabled={connecting}>
{connecting ? 'Connecting...' : `Connect ${appName}`}
{connecting
? i18n.t('chat.connectApp.connecting')
: i18n.t('chat.connectApp.connectButton', [appName])}
</Button>
<Button size="sm" variant="ghost" onClick={handleManual}>
Do it manually
{i18n.t('chat.connectApp.doItManually')}
</Button>
</div>
</div>

View File

@@ -1,5 +1,6 @@
import { Clock, X } from 'lucide-react'
import { type FC, useEffect, useState } from 'react'
import { i18n } from '#i18n'
import { Button } from '@/components/ui/button'
import {
BREADCRUMB_SCHEDULE_CLICKED_EVENT,
@@ -40,7 +41,9 @@ export const ScheduleSuggestionCard: FC<ScheduleSuggestionCardProps> = ({
if (dismissed) return null
const scheduleLabel =
scheduleType === 'daily' ? `daily at ${scheduleTime}` : 'every hour'
scheduleType === 'daily'
? i18n.t('chat.schedule.dailyAt', [scheduleTime])
: i18n.t('chat.schedule.everyHour')
const handleSchedule = () => {
track(BREADCRUMB_SCHEDULE_CLICKED_EVENT, {
@@ -75,7 +78,7 @@ export const ScheduleSuggestionCard: FC<ScheduleSuggestionCardProps> = ({
<div className="flex items-start gap-3 pr-6">
<Clock className="h-5 w-5 shrink-0 text-[var(--accent-orange)]" />
<div>
<p className="font-medium text-sm">Run this automatically?</p>
<p className="font-medium text-sm">{i18n.t('chat.schedule.title')}</p>
<p className="mt-1 text-muted-foreground text-xs">
&ldquo;{suggestedName}&rdquo; &mdash; I can run this {scheduleLabel}
</p>
@@ -84,10 +87,10 @@ export const ScheduleSuggestionCard: FC<ScheduleSuggestionCardProps> = ({
<div className="mt-3 flex gap-2">
<Button size="sm" onClick={handleSchedule}>
Schedule this task
{i18n.t('chat.schedule.scheduleButton')}
</Button>
<Button size="sm" variant="ghost" onClick={handleDismiss}>
Maybe later
{i18n.t('chat.schedule.maybeLater')}
</Button>
</div>
</div>

View File

@@ -0,0 +1,134 @@
extName: BrowserOS-Assistent
extActionTitle: BrowserOS fragen
chat:
input:
placeholderAgent: Was soll ich tun?
placeholderChat: Frage zu dieser Seite...
placeholderTranscribing: Transkribieren...
send: Senden
stop: Stop
stopRecording: Aufnahme stoppen
transcribing: Transkribieren
voiceInput: Spracheingabe
mode:
agent: Agent-Modus AN
chat: Chat-Modus AN
agentTooltip: KI kann browsen, klicken und navigieren
chatTooltip: KI kann nur lesen, nicht klicken oder navigieren
empty:
agentTitle: Agent zu Ihren Diensten
agentSubtitle: KI automatisiert Aufgaben und browset für Sie
chatTitle: Mit dieser Seite chatten
chatSubtitle: Fragen zur aktuellen Seite oder einem beliebigen Thema stellen
header:
changeProvider: KI-Anbieter wechseln
newConversation: Neues Gespräch
chatHistory: Chat-Verlauf
starOnGithub: Auf Github starren
settings: Einstellungen
error:
dailyLimitTitle: Tageslimit erreicht
connectionFailedTitle: Verbindung fehlgeschlagen
genericTitle: Fehler aufgetreten
connectionMessage: Verbindung zu BrowserOS-Agent nicht möglich. Folgen Sie den Anweisungen unten.
rateLimitMessage: Eigenen API-Key hinzufügen für unbegrenzte Nutzung.
troubleshootingLink: Fehlerbehebung anzeigen
kimiApiKeyLink: Anleitung zum Kimi API-Key
getApiKey: API-Key erhalten
tryAgain: Erneut versuchen
actions:
copy: Kopieren
copyToClipboard: In Zwischenablage kopieren
feedbackSubmitted: Feedback gesendet
like: Gefällt mir
likeTooltip: Antwort gefällt mir
dislike: Gefällt nicht
dislikeTooltip: Antwort gefällt nicht
feedbackTitle: Was ist schiefgelaufen?
feedbackDescription: Teilen Sie uns mit, was an dieser Antwort nicht stimmte.
feedbackPlaceholder: Kommentar (optional)
schedule:
title: Automatisch ausführen?
dailyAt: täglich um $1
everyHour: jede Stunde
scheduleButton: Aufgabe planen
maybeLater: Später vielleicht
connectApp:
connecting: Verbinden...
connectButton: $1 verbinden
authorizeTitle: $1 im geöffneten Tab autorisieren
authorizeDescription: Anmeldung abschließen, dann Button klicken.
authorizedButton: $1 autorisiert, weiter
skipButton: Überspringen, manuell
suggested: $1 vorgeschlagen
connected: $1 verbunden
continuedWithout: Ohne $1 fortfahren
connectForBetter: $1 für bessere Ergebnisse verbinden
doItManually: Manuell erledigen
connectedSuccess: $1 erfolgreich verbunden
connectFailed: Verbindung zu $1 fehlgeschlagen
footer:
attachTabs: Tabs anhängen (@)
selectWorkspace: Arbeitsbereich wählen
connectApps: Apps verbinden
selectedText:
removeTitle: Textauswahl entfernen
attachedTabs:
removeTab: Tab entfernen
newtab:
search:
placeholder: BrowserOS fragen oder $1 suchen...
branding:
alt: BrowserOS
selectedTab: Tab
appsNew: Neu!
appsTooltip: Direkt verbundene Apps liefern genauere und schnellere Antworten!
addWorkspace: Arbeitsbereich hinzufügen
tip:
label: "Tipp:"
nextTip: Nächster Tipp
dismiss: Schließen
signIn:
title: Daten synchronisieren
description: Anmelden zum Synchronisieren des Chat-Verlaufs.
dontAskAgain: Nicht wieder fragen
signInButton: Anmelden
importData:
title: Daten importieren
description: Lesezeichen, Verlauf und Passwörter aus Chrome importieren.
dontShowAgain: Nicht mehr anzeigen
openSettings: Importeinstellungen öffnen
shortcuts:
title: Tastenkürzel
description: Diese Kürzel für schnellere Navigation in BrowserOS
moreComingSoon: Weitere Kürzel folgen
sidebar:
nav:
home: Start
connectApps: Apps verbinden
scheduledTasks: Geplante Aufgaben
workflows: Workflows
skills: Skills
memory: Speicher
soul: Seele
settings: Einstellungen
footer:
about: Über BrowserOS
shortcuts: Kürzel
branding:
alt: BrowserOS
personal: Persönlich
updateProfile: Profil aktualisieren
signOut: Abmelden
signIn: Anmelden
common:
cancel: Abbrechen
submit: Absenden
save: Speichern
delete: Löschen
edit: Bearbeiten
close: Schließen
loading: Laden...
error: Fehler aufgetreten
apps: Apps
tabs: Tabs

View File

@@ -0,0 +1,169 @@
# Extension manifest
extName: BrowserOS Assistant
extActionTitle: Ask BrowserOS
# Chat input
chat:
input:
placeholderAgent: What should I do?
placeholderChat: Ask about this page...
placeholderTranscribing: Transcribing...
send: Send
stop: Stop
stopRecording: Stop recording
transcribing: Transcribing
voiceInput: Voice input
# Chat mode toggle
mode:
agent: Agent Mode ON
chat: Chat Mode ON
agentTooltip: AI can browse, click, and navigate
chatTooltip: AI can only read, cannot click or navigate
# Chat empty state
empty:
agentTitle: Agent at your service
agentSubtitle: Let AI automate tasks and browse for you
chatTitle: Chat with this page
chatSubtitle: Ask questions about the current page or any topic
# Chat header
header:
changeProvider: Change AI Provider
newConversation: New conversation
chatHistory: Chat history
starOnGithub: Star on Github
settings: Settings
# Chat errors
error:
dailyLimitTitle: Daily limit reached
connectionFailedTitle: Connection failed
genericTitle: Something went wrong
connectionMessage: "Unable to connect to BrowserOS agent. Follow below instructions."
rateLimitMessage: Add your own API key for unlimited usage.
troubleshootingLink: View troubleshooting guide
kimiApiKeyLink: Learn how to get a Kimi API key
getApiKey: get your API key
tryAgain: Try again
# Chat message actions
actions:
copy: Copy
copyToClipboard: Copy to clipboard
feedbackSubmitted: Feedback submitted
like: Like
likeTooltip: Like this response
dislike: Dislike
dislikeTooltip: Dislike this response
feedbackTitle: What went wrong?
feedbackDescription: Help us improve by sharing what was wrong with this response.
feedbackPlaceholder: Add a comment (optional)
# Schedule suggestion card
schedule:
title: Run this automatically?
dailyAt: "daily at $1"
everyHour: every hour
scheduleButton: Schedule this task
maybeLater: Maybe later
# Connect app card
connectApp:
connecting: Connecting...
connectButton: "Connect $1"
authorizeTitle: "Authorize $1 in the opened tab"
authorizeDescription: Complete the sign-in flow, then click the button below.
authorizedButton: "I've authorized $1, continue"
skipButton: "Skip, do it manually"
suggested: "$1 suggested"
connected: "Connected $1"
continuedWithout: "Continuing without $1"
connectForBetter: "Connect $1 for better results"
doItManually: Do it manually
connectedSuccess: "$1 connected successfully"
connectFailed: "Failed to connect $1"
# Chat footer
footer:
attachTabs: Attach tabs (@)
selectWorkspace: Select workspace folder
connectApps: Connect apps
# Selected text card
selectedText:
removeTitle: Remove selected text
# Attached tabs
attachedTabs:
removeTab: Remove tab
# Newtab page
newtab:
search:
placeholder: "Ask BrowserOS or search $1..."
branding:
alt: BrowserOS
selectedTab: Tab
appsNew: "New!"
appsTooltip: Apps directly connected will have more accurate and faster responses for your queries!
addWorkspace: Add workspace
tip:
label: "Tip:"
nextTip: Next tip
dismiss: Dismiss
# Sign in hint
signIn:
title: Sync your data
description: Sign in to sync conversation history to the cloud.
dontAskAgain: Don't ask again
signInButton: Sign in
# Import data hint
importData:
title: Import your data
description: "Bring bookmarks, history, and passwords from Chrome."
dontShowAgain: Don't show this again
openSettings: Open Import Settings
# Shortcuts dialog
shortcuts:
title: Keyboard Shortcuts
description: Use these shortcuts to navigate BrowserOS faster
moreComingSoon: More shortcuts coming soon
# Sidebar navigation
sidebar:
nav:
home: Home
connectApps: Connect Apps
scheduledTasks: Scheduled Tasks
workflows: Workflows
skills: Skills
memory: Memory
soul: Soul
settings: Settings
footer:
about: About BrowserOS
shortcuts: Shortcuts
branding:
alt: BrowserOS
personal: Personal
updateProfile: Update Profile
signOut: Sign out
signIn: Sign in
# Common / shared
common:
cancel: Cancel
submit: Submit
save: Save
delete: Delete
edit: Edit
close: Close
loading: Loading...
error: Something went wrong
apps: Apps
tabs: Tabs

View File

@@ -0,0 +1,134 @@
extName: Asistente BrowserOS
extActionTitle: Pregunta a BrowserOS
chat:
input:
placeholderAgent: ¿Qué debo hacer?
placeholderChat: Pregunta sobre esta página...
placeholderTranscribing: Transcribiendo...
send: Enviar
stop: Detener
stopRecording: Detener grabación
transcribing: Transcribiendo
voiceInput: Entrada de voz
mode:
agent: Modo Agente ON
chat: Modo Chat ON
agentTooltip: La IA puede navegar e interactuar
chatTooltip: Solo lectura, sin interacción
empty:
agentTitle: Agente a su servicio
agentSubtitle: Deja que la IA automatice y navegue por ti
chatTitle: Chatea con esta página
chatSubtitle: Pregunta sobre la página actual o cualquier tema
header:
changeProvider: Cambiar proveedor de IA
newConversation: Nueva conversación
chatHistory: Historial
starOnGithub: Star en GitHub
settings: Ajustes
error:
dailyLimitTitle: Límite diario alcanzado
connectionFailedTitle: Conexión fallida
genericTitle: Algo salió mal
connectionMessage: No se pudo conectar al agente. Sigue las instrucciones.
rateLimitMessage: Añade tu clave API para uso ilimitado.
troubleshootingLink: Ver guía de solución
kimiApiKeyLink: Cómo obtener clave API de Kimi
getApiKey: obtener tu clave API
tryAgain: Reintentar
actions:
copy: Copiar
copyToClipboard: Copiar al portapapeles
feedbackSubmitted: Feedback enviado
like: Me gusta
likeTooltip: Me gusta esta respuesta
dislike: No me gusta
dislikeTooltip: No me gusta esta respuesta
feedbackTitle: ¿Qué salió mal?
feedbackDescription: Cuéntanos qué salió mal para mejorar.
feedbackPlaceholder: Añade un comentario (opcional)
schedule:
title: ¿Ejecutar automáticamente?
dailyAt: diariamente a las $1
everyHour: cada hora
scheduleButton: Programar tarea
maybeLater: Más tarde
connectApp:
connecting: Conectando...
connectButton: Conectar $1
authorizeTitle: Autoriza $1 en la pestaña abierta
authorizeDescription: Completa el inicio de sesión y haz clic abajo.
authorizedButton: He autorizado $1, continuar
skipButton: Omitir, hacer manualmente
suggested: $1 sugerido
connected: $1 conectado
continuedWithout: Continuando sin $1
connectForBetter: Conecta $1 para mejores resultados
doItManually: Hacerlo manualmente
connectedSuccess: $1 conectado con éxito
connectFailed: Error al conectar $1
footer:
attachTabs: Adjuntar pestañas (@)
selectWorkspace: Seleccionar carpeta
connectApps: Conectar apps
selectedText:
removeTitle: Eliminar texto seleccionado
attachedTabs:
removeTab: Quitar pestaña
newtab:
search:
placeholder: Pregunta a BrowserOS o busca $1...
branding:
alt: BrowserOS
selectedTab: Pestaña
appsNew: ¡Nuevo!
appsTooltip: Las apps conectadas ofrecen respuestas más precisas y rápidas
addWorkspace: Añadir espacio de trabajo
tip:
label: "Consejo:"
nextTip: Siguiente consejo
dismiss: Descartar
signIn:
title: Sincroniza tus datos
description: Inicia sesión para sincronizar el historial en la nube.
dontAskAgain: No preguntar de nuevo
signInButton: Iniciar sesión
importData:
title: Importar tus datos
description: Importa marcadores, historial y contraseñas de Chrome.
dontShowAgain: No mostrar de nuevo
openSettings: Abrir ajustes de importación
shortcuts:
title: Atajos de teclado
description: Usa estos atajos para navegar más rápido
moreComingSoon: Más atajos pronto
sidebar:
nav:
home: Inicio
connectApps: Conectar Apps
scheduledTasks: Tareas programadas
workflows: Flujos
skills: Habilidades
memory: Memoria
soul: Alma
settings: Ajustes
footer:
about: Acerca de BrowserOS
shortcuts: Atajos
branding:
alt: BrowserOS
personal: Personal
updateProfile: Actualizar perfil
signOut: Cerrar sesión
signIn: Iniciar sesión
common:
cancel: Cancelar
submit: Enviar
save: Guardar
delete: Eliminar
edit: Editar
close: Cerrar
loading: Cargando...
error: Algo salió mal
apps: Apps
tabs: Pestañas

View File

@@ -0,0 +1,134 @@
extName: Assistant BrowserOS
extActionTitle: Demander à BrowserOS
chat:
input:
placeholderAgent: Que dois-je faire ?
placeholderChat: Demandez sur cette page...
placeholderTranscribing: Transcription...
send: Envoyer
stop: Arrêter
stopRecording: Arrêter l'enregistrement
transcribing: Transcription
voiceInput: Saisie vocale
mode:
agent: Mode agent activé
chat: Mode chat activé
agentTooltip: L'IA peut naviguer, cliquer et se déplacer
chatTooltip: L'IA peut lire mais pas cliquer ni naviguer
empty:
agentTitle: Agent à votre service
agentSubtitle: Laissez l'IA automatiser et naviguer pour vous
chatTitle: Discuter avec cette page
chatSubtitle: Posez des questions sur cette page ou tout sujet
header:
changeProvider: Changer fournisseur IA
newConversation: Nouvelle conversation
chatHistory: Historique
starOnGithub: Étoiler sur Github
settings: Paramètres
error:
dailyLimitTitle: Limite quotidienne atteinte
connectionFailedTitle: Connexion échouée
genericTitle: Une erreur s'est produite
connectionMessage: Impossible de connecter l'agent BrowserOS. Suivez les instructions ci-dessous.
rateLimitMessage: Ajoutez votre propre clé API pour un usage illimité.
troubleshootingLink: Voir le guide de dépannage
kimiApiKeyLink: Comment obtenir une clé API Kimi
getApiKey: obtenir votre clé API
tryAgain: Réessayer
actions:
copy: Copier
copyToClipboard: Copier dans le presse-papiers
feedbackSubmitted: Feedback envoyé
like: Utile
likeTooltip: Cette réponse est utile
dislike: Inutile
dislikeTooltip: Cette réponse est inutile
feedbackTitle: Quel est le problème ?
feedbackDescription: Aidez-nous à nous améliorer en indiquant le problème.
feedbackPlaceholder: Commentaire (facultatif)
schedule:
title: Exécuter automatiquement ?
dailyAt: tous les jours à $1
everyHour: toutes les heures
scheduleButton: Planifier cette tâche
maybeLater: Plus tard
connectApp:
connecting: Connexion...
connectButton: Connecter $1
authorizeTitle: Autoriser $1 dans l'onglet ouvert
authorizeDescription: Terminez la connexion, puis cliquez ci-dessous.
authorizedButton: J'ai autorisé $1, continuer
skipButton: Passer, faire manuellement
suggested: $1 suggéré
connected: $1 connecté
continuedWithout: Continuer sans $1
connectForBetter: Connectez $1 pour de meilleurs résultats
doItManually: Faire manuellement
connectedSuccess: $1 connecté avec succès
connectFailed: Impossible de connecter $1
footer:
attachTabs: Joindre onglets (@)
selectWorkspace: Choisir l'espace de travail
connectApps: Connecter apps
selectedText:
removeTitle: Supprimer le texte sélectionné
attachedTabs:
removeTab: Supprimer l'onglet
newtab:
search:
placeholder: Demandez à BrowserOS ou cherchez $1...
branding:
alt: BrowserOS
selectedTab: Onglet
appsNew: Nouveau !
appsTooltip: Les apps connectées directement offrent des réponses plus précises et rapides !
addWorkspace: Ajouter un espace de travail
tip:
label: "Astuce :"
nextTip: Astuce suivante
dismiss: Ignorer
signIn:
title: Synchroniser vos données
description: Connectez-vous pour synchroniser l'historique dans le cloud.
dontAskAgain: Ne plus demander
signInButton: Se connecter
importData:
title: Importer vos données
description: Importez marque-pages, historique et mots de passe depuis Chrome.
dontShowAgain: Ne plus afficher
openSettings: Ouvrir les paramètres d'importation
shortcuts:
title: Raccourcis clavier
description: Utilisez ces raccourcis pour naviguer plus vite dans BrowserOS
moreComingSoon: Plus de raccourcis bientôt
sidebar:
nav:
home: Accueil
connectApps: Apps connectées
scheduledTasks: Tâches planifiées
workflows: Workflows
skills: Compétences
memory: Mémoire
soul: Âme
settings: Paramètres
footer:
about: À propos de BrowserOS
shortcuts: Raccourcis
branding:
alt: BrowserOS
personal: Personnel
updateProfile: Mettre à jour le profil
signOut: Déconnexion
signIn: Connexion
common:
cancel: Annuler
submit: Valider
save: Enregistrer
delete: Supprimer
edit: Modifier
close: Fermer
loading: Chargement...
error: Une erreur s'est produite
apps: Apps
tabs: Onglets

View File

@@ -0,0 +1,134 @@
extName: BrowserOS アシスタント
extActionTitle: BrowserOS に質問
chat:
input:
placeholderAgent: 何をしますか?
placeholderChat: このページについて...
placeholderTranscribing: 文字起こし中...
send: 送信
stop: 停止
stopRecording: 録音停止
transcribing: 文字起こし中
voiceInput: 音声入力
mode:
agent: エージェントモードON
chat: チャットモードON
agentTooltip: 閲覧・クリック・ナビゲート可能
chatTooltip: 閲覧のみ、クリック・ナビゲート不可
empty:
agentTitle: エージェントが対応します
agentSubtitle: AIにタスク自動化と閲覧をお任せ
chatTitle: このページとチャット
chatSubtitle: 現在のページやトピックについて質問
header:
changeProvider: AIプロバイダー変更
newConversation: 新規会話
chatHistory: 履歴
starOnGithub: GitHubでスター
settings: 設定
error:
dailyLimitTitle: 1日の制限に達しました
connectionFailedTitle: 接続失敗
genericTitle: エラーが発生しました
connectionMessage: BrowserOSエージェントに接続できません。以下の手順に従ってください。
rateLimitMessage: 無制限利用にはAPIキーを追加してください。
troubleshootingLink: トラブルシューティングを見る
kimiApiKeyLink: Kimi APIキーの取得方法
getApiKey: APIキーを取得
tryAgain: 再試行
actions:
copy: コピー
copyToClipboard: クリップボードにコピー
feedbackSubmitted: フィードバック送信済み
like: いいね
likeTooltip: この回答にいいね
dislike: よくないね
dislikeTooltip: この回答に不満
feedbackTitle: 何が問題でしたか?
feedbackDescription: 回答の問題点を教えてください
feedbackPlaceholder: コメントを追加(任意)
schedule:
title: 自動実行しますか?
dailyAt: 毎日 $1
everyHour: 毎時間
scheduleButton: タスクを予約
maybeLater: 後で
connectApp:
connecting: 接続中...
connectButton: $1 に接続
authorizeTitle: 開いたタブで $1 を承認
authorizeDescription: サインインを完了し、下のボタンをクリックしてください。
authorizedButton: $1 の承認完了、続ける
skipButton: スキップして手動で実行
suggested: $1 を推奨
connected: $1 に接続済み
continuedWithout: $1 なしで続行
connectForBetter: より良い結果のため $1 に接続
doItManually: 手動で実行
connectedSuccess: $1 の接続に成功
connectFailed: $1 の接続に失敗
footer:
attachTabs: タブを添付(@)
selectWorkspace: ワークスペースフォルダを選択
connectApps: アプリを接続
selectedText:
removeTitle: 選択テキストを削除
attachedTabs:
removeTab: タブを削除
newtab:
search:
placeholder: BrowserOSに質問、または$1を検索...
branding:
alt: BrowserOS
selectedTab: タブ
appsNew: 新着!
appsTooltip: 直接接続したアプリはより正確・高速な回答を提供します!
addWorkspace: ワークスペースを追加
tip:
label: ヒント:
nextTip: 次のヒント
dismiss: 閉じる
signIn:
title: データを同期
description: 会話履歴をクラウドに同期するにはサインインしてください。
dontAskAgain: 今後表示しない
signInButton: サインイン
importData:
title: データをインポート
description: Chromeからブックマーク、履歴、パスワードを取り込みます。
dontShowAgain: 今後表示しない
openSettings: インポート設定を開く
shortcuts:
title: キーボードショートカット
description: BrowserOSをより速く操作するためのショートカット
moreComingSoon: さらにショートカットを追加予定
sidebar:
nav:
home: ホーム
connectApps: アプリ接続
scheduledTasks: 予定タスク
workflows: ワークフロー
skills: スキル
memory: メモリ
soul: ソウル
settings: 設定
footer:
about: BrowserOSについて
shortcuts: ショートカット
branding:
alt: BrowserOS
personal: 個人
updateProfile: プロフィール更新
signOut: サインアウト
signIn: サインイン
common:
cancel: キャンセル
submit: 送信
save: 保存
delete: 削除
edit: 編集
close: 閉じる
loading: 読み込み中...
error: エラーが発生しました
apps: アプリ
tabs: タブ

View File

@@ -0,0 +1,134 @@
extName: BrowserOS Assistant
extActionTitle: BrowserOS에게 물어보기
chat:
input:
placeholderAgent: 무엇을 할까요?
placeholderChat: 이 페이지에 대해 질문...
placeholderTranscribing: 전사 중...
send: 전송
stop: 중지
stopRecording: 녹음 중지
transcribing: 전사 중
voiceInput: 음성 입력
mode:
agent: 에이전트 모드 ON
chat: 채팅 모드 ON
agentTooltip: AI가 탐색, 클릭, 이동 가능
chatTooltip: AI는 읽기만 가능, 클릭/이동 불가
empty:
agentTitle: 에이전트가 대기 중
agentSubtitle: AI가 작업을 자동화하고 탐색합니다
chatTitle: 이 페이지와 채팅
chatSubtitle: 현재 페이지나 주제에 대해 질문하세요
header:
changeProvider: AI 제공자 변경
newConversation: 새 대화
chatHistory: 대화 기록
starOnGithub: Github에서 Star
settings: 설정
error:
dailyLimitTitle: 일일 한도 도달
connectionFailedTitle: 연결 실패
genericTitle: 문제가 발생했습니다
connectionMessage: BrowserOS 에이전트에 연결할 수 없습니다. 아래 안내를 따르세요.
rateLimitMessage: 무제한 사용을 위해 API 키를 추가하세요.
troubleshootingLink: 문제 해결 가이드 보기
kimiApiKeyLink: Kimi API 키 얻는 방법 알아보기
getApiKey: API 키 받기
tryAgain: 다시 시도
actions:
copy: 복사
copyToClipboard: 클립보드에 복사
feedbackSubmitted: 피드백 전송됨
like: 좋아요
likeTooltip: 이 응답이 좋아요
dislike: 싫어요
dislikeTooltip: 이 응답이 싫어요
feedbackTitle: 무엇이 잘못되었나요?
feedbackDescription: 이 응답의 문제를 공유하여 개선을 도와주세요.
feedbackPlaceholder: 댓글 추가 (선택사항)
schedule:
title: 자동으로 실행할까요?
dailyAt: 매일 $1
everyHour: 매시간
scheduleButton: 작업 예약
maybeLater: 나중에
connectApp:
connecting: 연결 중...
connectButton: $1 연결
authorizeTitle: 열린 탭에서 $1 인증
authorizeDescription: 로그인을 완료한 후 아래 버튼을 클릭하세요.
authorizedButton: $1 인증 완료, 계속하기
skipButton: 건너뛰기, 수동으로 실행
suggested: $1 추천
connected: $1 연결됨
continuedWithout: $1 없이 계속
connectForBetter: 더 나은 결과를 위해 $1 연결
doItManually: 수동으로 실행
connectedSuccess: $1 연결 성공
connectFailed: $1 연결 실패
footer:
attachTabs: 탭 첨부 (@)
selectWorkspace: 작업 폴더 선택
connectApps: 앱 연결
selectedText:
removeTitle: 선택한 텍스트 제거
attachedTabs:
removeTab: 탭 제거
newtab:
search:
placeholder: BrowserOS에게 묻거나 $1 검색...
branding:
alt: BrowserOS
selectedTab:
appsNew: 신규!
appsTooltip: 직접 연결된 앱은 더 정확하고 빠른 응답을 제공합니다!
addWorkspace: 작업 공간 추가
tip:
label: "팁:"
nextTip: 다음 팁
dismiss: 닫기
signIn:
title: 데이터 동기화
description: 대화 기록을 클라우드에 동기화하려면 로그인하세요.
dontAskAgain: 다시 묻지 않기
signInButton: 로그인
importData:
title: 데이터 가져오기
description: Chrome에서 북마크, 기록, 비밀번호를 가져옵니다.
dontShowAgain: 다시 표시하지 않기
openSettings: 가져오기 설정 열기
shortcuts:
title: 키보드 단축키
description: BrowserOS를 더 빠르게 탐색하려면 이 단축키를 사용하세요.
moreComingSoon: 더 많은 단축키가 곧 제공됩니다
sidebar:
nav:
home:
connectApps: 앱 연결
scheduledTasks: 예약된 작업
workflows: 워크플로우
skills: 스킬
memory: 메모리
soul: 소울
settings: 설정
footer:
about: BrowserOS 정보
shortcuts: 단축키
branding:
alt: BrowserOS
personal: 개인
updateProfile: 프로필 업데이트
signOut: 로그아웃
signIn: 로그인
common:
cancel: 취소
submit: 제출
save: 저장
delete: 삭제
edit: 편집
close: 닫기
loading: 로딩 중...
error: 문제가 발생했습니다
apps:
tabs:

View File

@@ -0,0 +1,134 @@
extName: Assistente BrowserOS
extActionTitle: Pergunte ao BrowserOS
chat:
input:
placeholderAgent: O que devo fazer?
placeholderChat: Pergunte sobre esta página...
placeholderTranscribing: Transcrevendo...
send: Enviar
stop: Parar
stopRecording: Parar gravação
transcribing: Transcrevendo
voiceInput: Entrada de voz
mode:
agent: Modo Agente ATIVO
chat: Modo Chat ATIVO
agentTooltip: A IA pode navegar, clicar e acessar sites
chatTooltip: A IA só lê, não pode clicar ou navegar
empty:
agentTitle: Agente à sua disposição
agentSubtitle: Deixe a IA automatizar tarefas e navegar por você
chatTitle: Converse com esta página
chatSubtitle: Faça perguntas sobre a página atual ou qualquer tópico
header:
changeProvider: Mudar provedor de IA
newConversation: Nova conversa
chatHistory: Histórico de chat
starOnGithub: Estrelar no GitHub
settings: Configurações
error:
dailyLimitTitle: Limite diário atingido
connectionFailedTitle: Falha na conexão
genericTitle: Algo deu errado
connectionMessage: Não foi possível conectar ao agente BrowserOS. Siga as instruções abaixo.
rateLimitMessage: Adicione sua própria chave de API para uso ilimitado.
troubleshootingLink: Ver guia de solução de problemas
kimiApiKeyLink: Saiba como obter uma chave de API Kimi
getApiKey: obtenha sua chave de API
tryAgain: Tentar novamente
actions:
copy: Copiar
copyToClipboard: Copiar para a área de transferência
feedbackSubmitted: Feedback enviado
like: Curtir
likeTooltip: Curtir esta resposta
dislike: Descurtir
dislikeTooltip: Não curtir esta resposta
feedbackTitle: O que deu errado?
feedbackDescription: Ajude-nos a melhorar informando o que estava errado.
feedbackPlaceholder: Adicione um comentário (opcional)
schedule:
title: Executar isso automaticamente?
dailyAt: diariamente às $1
everyHour: a cada hora
scheduleButton: Agendar esta tarefa
maybeLater: Talvez depois
connectApp:
connecting: Conectando...
connectButton: Conectar $1
authorizeTitle: Autorize $1 na aba aberta
authorizeDescription: Complete o fluxo de login, depois clique no botão abaixo.
authorizedButton: Autorizei $1, continuar
skipButton: Pular, fazer manualmente
suggested: $1 sugerido
connected: $1 conectado
continuedWithout: Continuando sem $1
connectForBetter: Conecte $1 para melhores resultados
doItManually: Fazer manualmente
connectedSuccess: $1 conectado com sucesso
connectFailed: Falha ao conectar $1
footer:
attachTabs: Anexar abas (@)
selectWorkspace: Selecionar workspace
connectApps: Conectar apps
selectedText:
removeTitle: Remover texto selecionado
attachedTabs:
removeTab: Remover aba
newtab:
search:
placeholder: Pergunte ao BrowserOS ou pesquise em $1...
branding:
alt: BrowserOS
selectedTab: Aba
appsNew: Novo!
appsTooltip: Apps conectados diretamente oferecem respostas mais precisas e rápidas!
addWorkspace: Adicionar workspace
tip:
label: "Dica:"
nextTip: Próxima dica
dismiss: Fechar
signIn:
title: Sincronize seus dados
description: Entre para sincronizar o histórico de conversas na nuvem.
dontAskAgain: Não perguntar novamente
signInButton: Entrar
importData:
title: Importe seus dados
description: Traga favoritos, histórico e senhas do Chrome.
dontShowAgain: Não mostrar novamente
openSettings: Abrir Configurações de Importação
shortcuts:
title: Atalhos de Teclado
description: Use atalhos para navegar mais rápido no BrowserOS
moreComingSoon: Mais atalhos em breve
sidebar:
nav:
home: Início
connectApps: Conectar Apps
scheduledTasks: Tarefas Agendadas
workflows: Fluxos de trabalho
skills: Habilidades
memory: Memória
soul: Alma
settings: Configurações
footer:
about: Sobre o BrowserOS
shortcuts: Atalhos
branding:
alt: BrowserOS
personal: Pessoal
updateProfile: Atualizar Perfil
signOut: Sair
signIn: Entrar
common:
cancel: Cancelar
submit: Enviar
save: Salvar
delete: Excluir
edit: Editar
close: Fechar
loading: Carregando...
error: Algo deu errado
apps: Apps
tabs: Abas

View File

@@ -0,0 +1,134 @@
extName: BrowserOS 助手
extActionTitle: 询问 BrowserOS
chat:
input:
placeholderAgent: 需要做什么?
placeholderChat: 询问此页面...
placeholderTranscribing: 正在转录...
send: 发送
stop: 停止
stopRecording: 停止录音
transcribing: 转录中
voiceInput: 语音输入
mode:
agent: Agent 模式开启
chat: 聊天模式开启
agentTooltip: AI 可浏览、点击、导航
chatTooltip: AI 仅可阅读,不可点击导航
empty:
agentTitle: Agent 为您服务
agentSubtitle: 让 AI 自动执行任务、代为浏览
chatTitle: 与此页对话
chatSubtitle: 询问当前页面或任意话题
header:
changeProvider: 切换 AI 提供商
newConversation: 新建对话
chatHistory: 对话历史
starOnGithub: 在 GitHub 标星
settings: 设置
error:
dailyLimitTitle: 已达每日上限
connectionFailedTitle: 连接失败
genericTitle: 出错了
connectionMessage: 无法连接 BrowserOS Agent请按以下步骤操作。
rateLimitMessage: 添加个人 API 密钥以解除限制。
troubleshootingLink: 查看排查指南
kimiApiKeyLink: 了解如何获取 Kimi API 密钥
getApiKey: 获取 API 密钥
tryAgain: 重试
actions:
copy: 复制
copyToClipboard: 复制到剪贴板
feedbackSubmitted: 反馈已提交
like: 点赞
likeTooltip: 为此回复点赞
dislike: 点踩
dislikeTooltip: 为此回复点踩
feedbackTitle: 出了什么问题?
feedbackDescription: 分享此回复的问题,助我们改进。
feedbackPlaceholder: 添加评论(可选)
schedule:
title: 自动运行此任务?
dailyAt: 每天 $1
everyHour: 每小时
scheduleButton: 安排此任务
maybeLater: 稍后
connectApp:
connecting: 连接中...
connectButton: 连接 $1
authorizeTitle: 在已打开的标签页中授权 $1
authorizeDescription: 完成登录后点击下方按钮。
authorizedButton: 已授权 $1继续
skipButton: 跳过,手动操作
suggested: 建议使用 $1
connected: 已连接 $1
continuedWithout: 继续,不使用 $1
connectForBetter: 连接 $1 以获得更好结果
doItManually: 手动操作
connectedSuccess: $1 连接成功
connectFailed: $1 连接失败
footer:
attachTabs: 附加标签 (@)
selectWorkspace: 选择工作区文件夹
connectApps: 连接应用
selectedText:
removeTitle: 移除选中文本
attachedTabs:
removeTab: 移除标签
newtab:
search:
placeholder: 询问 BrowserOS 或搜索 $1...
branding:
alt: BrowserOS
selectedTab: 标签
appsNew: 新!
appsTooltip: 直接连接的应用回复更准确、更快速!
addWorkspace: 添加工作区
tip:
label: 提示:
nextTip: 下一条提示
dismiss: 忽略
signIn:
title: 同步数据
description: 登录以同步对话历史至云端。
dontAskAgain: 不再询问
signInButton: 登录
importData:
title: 导入数据
description: 从 Chrome 导入书签、历史和密码。
dontShowAgain: 不再显示
openSettings: 打开导入设置
shortcuts:
title: 快捷键
description: 使用快捷键更快操作 BrowserOS
moreComingSoon: 更多快捷键即将推出
sidebar:
nav:
home: 首页
connectApps: 连接应用
scheduledTasks: 定时任务
workflows: 工作流
skills: 技能
memory: 记忆
soul: 灵魂
settings: 设置
footer:
about: 关于 BrowserOS
shortcuts: 快捷键
branding:
alt: BrowserOS
personal: 个人版
updateProfile: 更新资料
signOut: 退出
signIn: 登录
common:
cancel: 取消
submit: 提交
save: 保存
delete: 删除
edit: 编辑
close: 关闭
loading: 加载中...
error: 出错了
apps: 应用
tabs: 标签

View File

@@ -15,7 +15,8 @@
"lint:fix": "bunx biome check --write --unsafe",
"clean:cache": "rm -rf node_modules/.cache && rm -rf .output/ && rm -rf .wxt/",
"codegen": "bun --env-file=.env.development graphql-codegen --config codegen.ts",
"codegen:watch": "graphql-codegen --config codegen.ts --watch"
"codegen:watch": "graphql-codegen --config codegen.ts --watch",
"translate": "bun run scripts/translate.ts"
},
"dependencies": {
"@ai-sdk/react": "^3.0.96",
@@ -96,6 +97,7 @@
},
"devDependencies": {
"@0no-co/graphqlsp": "^1.15.2",
"@ai-sdk/openai-compatible": "^2.0.35",
"@eslint/compat": "^2.0.1",
"@graphql-codegen/cli": "^6.1.1",
"@graphql-codegen/client-preset": "^5.2.2",

View File

@@ -0,0 +1,261 @@
/* biome-ignore-all lint/suspicious/noConsole: CLI script requires console output */
/* biome-ignore-all lint/style/noProcessEnv: CLI script reads env vars directly */
/**
* Auto-translate locale files using any OpenAI-compatible API.
*
* Usage:
* bun run scripts/translate.ts # translate all target languages
* bun run scripts/translate.ts --lang=zh_CN # translate one language
* bun run scripts/translate.ts --lang=ja --dry-run # preview without writing
*
* Environment variables:
* TRANSLATE_API_KEY — API key (required)
* TRANSLATE_BASE_URL — Base URL (default: https://api.anthropic.com/v1)
* TRANSLATE_MODEL — Model ID (default: claude-sonnet-4-20250514)
*/
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { createOpenAICompatible } from '@ai-sdk/openai-compatible'
import { generateText } from 'ai'
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'
const LOCALES_DIR = join(import.meta.dir, '..', 'locales')
const SOURCE_LOCALE = 'en'
// Target languages to translate to
const TARGET_LOCALES: Record<string, string> = {
zh_CN: 'Chinese Simplified',
ja: 'Japanese',
ko: 'Korean',
es: 'Spanish',
fr: 'French',
de: 'German',
pt_BR: 'Portuguese (Brazil)',
}
// Terms that should NOT be translated
const PRESERVE_TERMS = [
'BrowserOS',
'GitHub',
'Github',
'OpenAI',
'Anthropic',
'Gemini',
'Claude',
'Gmail',
'Slack',
'Linear',
'Notion',
'Kimi',
'API',
'MCP',
'OAuth',
]
function flattenYaml(
obj: Record<string, unknown>,
prefix = '',
): Record<string, string> {
const result: Record<string, string> = {}
for (const [key, value] of Object.entries(obj)) {
const fullKey = prefix ? `${prefix}.${key}` : key
if (typeof value === 'string') {
result[fullKey] = value
} else if (typeof value === 'object' && value !== null) {
Object.assign(
result,
flattenYaml(value as Record<string, unknown>, fullKey),
)
}
}
return result
}
function unflattenYaml(flat: Record<string, string>): Record<string, unknown> {
const result: Record<string, unknown> = {}
for (const [key, value] of Object.entries(flat)) {
const parts = key.split('.')
let current = result
for (let i = 0; i < parts.length - 1; i++) {
if (!current[parts[i]] || typeof current[parts[i]] !== 'object') {
current[parts[i]] = {}
}
current = current[parts[i]] as Record<string, unknown>
}
current[parts[parts.length - 1]] = value
}
return result
}
async function translateKeys(
keys: Record<string, string>,
targetLang: string,
targetName: string,
): Promise<Record<string, string>> {
const apiKey = process.env.TRANSLATE_API_KEY
if (!apiKey) {
throw new Error(
'TRANSLATE_API_KEY environment variable is required. Set it before running.',
)
}
const baseURL =
process.env.TRANSLATE_BASE_URL || 'https://api.anthropic.com/v1'
const modelId = process.env.TRANSLATE_MODEL || 'claude-sonnet-4-20250514'
const provider = createOpenAICompatible({
name: 'translate-provider',
baseURL,
apiKey,
})
const keysText = Object.entries(keys)
.map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
.join('\n')
const { text } = await generateText({
model: provider.chatModel(modelId),
prompt: `Translate the following UI strings from English to ${targetName} (${targetLang}).
These are for a browser extension called BrowserOS — an AI-powered browser assistant.
Rules:
- Keep translations concise — they must fit in the same UI space as the English text.
- Preserve $1, $2 substitution placeholders exactly as-is.
- Do NOT translate these proper nouns: ${PRESERVE_TERMS.join(', ')}
- Return ONLY a valid JSON object mapping keys to translated strings. No markdown, no explanations.
Keys to translate:
${keysText}`,
})
// Extract JSON from response (handle potential markdown wrapping)
const jsonMatch = text.match(/\{[\s\S]*\}/)
if (!jsonMatch) {
throw new Error(`Failed to parse translation response for ${targetLang}`)
}
return JSON.parse(jsonMatch[0]) as Record<string, string>
}
async function main() {
const args = process.argv.slice(2)
const langArg = args.find((a) => a.startsWith('--lang='))
const dryRun = args.includes('--dry-run')
const targetLang = langArg?.split('=')[1]
// Read source locale
const sourcePath = join(LOCALES_DIR, `${SOURCE_LOCALE}.yml`)
if (!existsSync(sourcePath)) {
console.error(`Source locale not found: ${sourcePath}`)
process.exit(1)
}
const sourceYaml = parseYaml(readFileSync(sourcePath, 'utf-8')) as Record<
string,
unknown
>
const sourceFlat = flattenYaml(sourceYaml)
const locales = targetLang
? { [targetLang]: TARGET_LOCALES[targetLang] || targetLang }
: TARGET_LOCALES
for (const [locale, langName] of Object.entries(locales)) {
console.log(`\n--- ${langName} (${locale}) ---`)
const targetPath = join(LOCALES_DIR, `${locale}.yml`)
let existingFlat: Record<string, string> = {}
if (existsSync(targetPath)) {
const existingYaml = parseYaml(
readFileSync(targetPath, 'utf-8'),
) as Record<string, unknown>
existingFlat = flattenYaml(existingYaml)
}
// Find keys that need translation (new or changed in source)
const keysToTranslate: Record<string, string> = {}
for (const [key, value] of Object.entries(sourceFlat)) {
if (!existingFlat[key]) {
keysToTranslate[key] = value
}
}
// Find keys to remove (no longer in source)
const keysToRemove = Object.keys(existingFlat).filter(
(key) => !sourceFlat[key],
)
if (
Object.keys(keysToTranslate).length === 0 &&
keysToRemove.length === 0
) {
console.log(' ✓ Up to date — no changes needed')
continue
}
console.log(
` ${Object.keys(keysToTranslate).length} key(s) to translate, ${keysToRemove.length} key(s) to remove`,
)
if (dryRun) {
if (Object.keys(keysToTranslate).length > 0) {
console.log(' New/changed keys:')
for (const key of Object.keys(keysToTranslate)) {
console.log(` + ${key}`)
}
}
if (keysToRemove.length > 0) {
console.log(' Removed keys:')
for (const key of keysToRemove) {
console.log(` - ${key}`)
}
}
continue
}
// Translate new keys
let translated: Record<string, string> = {}
if (Object.keys(keysToTranslate).length > 0) {
console.log(' Translating...')
translated = await translateKeys(keysToTranslate, locale, langName)
console.log(` ✓ Translated ${Object.keys(translated).length} key(s)`)
}
// Merge: existing (preserving human edits) + new translations - removed keys
const merged = { ...existingFlat, ...translated }
for (const key of keysToRemove) {
delete merged[key]
}
// Validate
const missingPlaceholders: string[] = []
for (const [key, value] of Object.entries(merged)) {
const sourcePlaceholders = (sourceFlat[key] || '').match(/\$\d+/g) || []
const translatedPlaceholders = value.match(/\$\d+/g) || []
if (sourcePlaceholders.length !== translatedPlaceholders.length) {
missingPlaceholders.push(key)
}
}
if (missingPlaceholders.length > 0) {
console.warn(
` ⚠ Placeholder mismatch in: ${missingPlaceholders.join(', ')}`,
)
}
// Write
const nestedYaml = unflattenYaml(merged)
const yamlStr = stringifyYaml(nestedYaml, { lineWidth: 0 })
writeFileSync(targetPath, yamlStr, 'utf-8')
console.log(` ✓ Written to ${targetPath}`)
}
console.log('\nDone!')
}
main().catch((err) => {
console.error(err)
process.exit(1)
})

View File

@@ -5,7 +5,8 @@
"allowImportingTsExtensions": true,
"jsx": "react-jsx",
"paths": {
"@/*": ["./*"]
"@/*": ["./*"],
"#i18n": ["./.wxt/i18n/index.ts"]
},
"plugins": [
{

View File

@@ -17,9 +17,10 @@ const apiPattern = apiUrl.port
// Extension ID will be bflpfmnmnokmjhmgnolecpppdbdophmk
export default defineConfig({
outDir: 'dist',
modules: ['@wxt-dev/module-react'],
modules: ['@wxt-dev/module-react', '@wxt-dev/i18n/module'],
manifest: {
name: 'Assistant',
name: '__MSG_extName__',
default_locale: 'en',
key: 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvBDAaDRvv61NpBeLR8etBRw82lv9VJO3sz/mA26gDzWKtVuzW4DXCl8Zfj5oWmoXLTfv3aiTigUXo/LHOoGpSucEVroMmAc7cgu2KuQ1fZPpMvYa0npD/m4h89360q8Oz0oKKaZGS905IJ04M2IkF4CuU3YEHFJBWb+cUyK9H8YVugelYbPD0IVs63T1SkGbh/t/Tfb2DpkinduSO8+x26sKydm30SRt+iZ2+7Nolcdum3LExInUiX2Pgb65Jb+mVw8NqyTVJyCEp8uq0cSHomWFQirSJ80tsDhISp4btwaRKHrXqovQx9XHQv4hCd+3LuB830eUEVMUNuCO+OyPxQIDAQAB',
update_url: 'https://cdn.browseros.com/extensions/update-manifest.xml',
// update_url: 'https://cdn.browseros.com/extensions/update-manifest.alpha.xml',
@@ -50,7 +51,7 @@ export default defineConfig({
48: 'icon/48.png',
128: 'icon/128.png',
},
default_title: 'Ask BrowserOS',
default_title: '__MSG_extActionTitle__',
},
permissions: [
'topSites',

View File

@@ -4,6 +4,9 @@
"workspaces": {
"": {
"name": "browseros-monorepo",
"dependencies": {
"@wxt-dev/i18n": "^0.2.5",
},
"devDependencies": {
"@aws-sdk/client-s3": "^3.933.0",
"@biomejs/biome": "2.4.8",
@@ -19,6 +22,7 @@
"rimraf": "^6.0.1",
"typedoc": "^0.28.15",
"typescript": "^5.9.2",
"yaml": "^2.8.2",
},
},
"apps/agent": {
@@ -103,6 +107,7 @@
},
"devDependencies": {
"@0no-co/graphqlsp": "^1.15.2",
"@ai-sdk/openai-compatible": "^2.0.35",
"@eslint/compat": "^2.0.1",
"@graphql-codegen/cli": "^6.1.1",
"@graphql-codegen/client-preset": "^5.2.2",
@@ -287,11 +292,11 @@
"@ai-sdk/openai": ["@ai-sdk/openai@3.0.30", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.15" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YDht3t7TDyWKP+JYZp20VuYqSjyF2brHYh47GGFDUPf2wZiqNQ263ecL+quar2bP3GZ3BeQA8f0m2B7UwLPR+g=="],
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.30", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.15" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iTjumHf1/u4NhjXYFn/aONM2GId3/o7J1Lp5ql8FCbgIMyRwrmanR5xy1S3aaVkfTscuDvLTzWiy1mAbGzK3nQ=="],
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-g3wA57IAQFb+3j4YuFndgkUdXyRETZVvbfAWM+UX7bZSxA3xjes0v3XKgIdKdekPtDGsh4ZX2byHD0gJIMPfiA=="],
"@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.15", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8XiKWbemmCbvNN0CLR9u3PQiet4gtEVIrX4zzLxnCj06AwsEDJwJVBbKrEI4t6qE8XRSIvU2irka0dcpziKW6w=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="],
"@ai-sdk/react": ["@ai-sdk/react@3.0.99", "", { "dependencies": { "@ai-sdk/provider-utils": "4.0.15", "ai": "6.0.97", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" } }, "sha512-xMsp5br4Dpr/3BYq/jrE8q4YLgViU1KHVq8VB0+dzdLJFU3jKA83uoxpbWqzV/edQOBPgGBSb2CgmV5v77rvzA=="],
@@ -1997,6 +2002,8 @@
"@wxt-dev/browser": ["@wxt-dev/browser@0.1.37", "", { "dependencies": { "@types/filesystem": "*", "@types/har-format": "*" } }, "sha512-I32XWCNRy2W6UgbaVXz8BHGBGtm8urGRRBrcNLagUBXTrBi7wCE6zWePUvvK+nUl7qUCZ7iQ1ufdP0c1DEWisw=="],
"@wxt-dev/i18n": ["@wxt-dev/i18n@0.2.5", "", { "dependencies": { "@wxt-dev/browser": "^0.1.37", "chokidar": "^5.0.0", "confbox": "^0.1.8 || ^0.2.2", "fast-glob": "^3.3.3" }, "peerDependencies": { "wxt": ">=0.19.7" }, "optionalPeers": ["wxt"] }, "sha512-B9EwzR7eTIZv5HQSsQL9/NaBHXH7G/esEtxMCoBMQyHx6QWWJNMJLX2C4z8slQUbblvlt4bPa+/zIC1IrWc1LA=="],
"@wxt-dev/module-react": ["@wxt-dev/module-react@1.1.5", "", { "dependencies": { "@vitejs/plugin-react": "^4.4.1 || ^5.0.0" }, "peerDependencies": { "wxt": ">=0.19.16" } }, "sha512-KgsUrsgH5rBT8MwiipnDEOHBXmLvTIdFICrI7KjngqSf9DpVRn92HsKmToxY0AYpkP19hHWta2oNYFTzmmm++g=="],
"@wxt-dev/storage": ["@wxt-dev/storage@1.2.8", "", { "dependencies": { "@wxt-dev/browser": "^0.1.37", "async-mutex": "^0.5.0", "dequal": "^2.0.3" } }, "sha512-GWCFKgF5+d7eslOxUDFC70ypA9njupmJb1nQM8uZoX0J3sWT2BO5xJLzb1sYahWAfID9p2BMtnUBN1lkWxPsbQ=="],
@@ -4417,8 +4424,24 @@
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.15", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8XiKWbemmCbvNN0CLR9u3PQiet4gtEVIrX4zzLxnCj06AwsEDJwJVBbKrEI4t6qE8XRSIvU2irka0dcpziKW6w=="],
"@ai-sdk/amazon-bedrock/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="],
"@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.15", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8XiKWbemmCbvNN0CLR9u3PQiet4gtEVIrX4zzLxnCj06AwsEDJwJVBbKrEI4t6qE8XRSIvU2irka0dcpziKW6w=="],
"@ai-sdk/azure/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.15", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8XiKWbemmCbvNN0CLR9u3PQiet4gtEVIrX4zzLxnCj06AwsEDJwJVBbKrEI4t6qE8XRSIvU2irka0dcpziKW6w=="],
"@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.15", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8XiKWbemmCbvNN0CLR9u3PQiet4gtEVIrX4zzLxnCj06AwsEDJwJVBbKrEI4t6qE8XRSIvU2irka0dcpziKW6w=="],
"@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.15", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8XiKWbemmCbvNN0CLR9u3PQiet4gtEVIrX4zzLxnCj06AwsEDJwJVBbKrEI4t6qE8XRSIvU2irka0dcpziKW6w=="],
"@ai-sdk/mcp/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.15", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8XiKWbemmCbvNN0CLR9u3PQiet4gtEVIrX4zzLxnCj06AwsEDJwJVBbKrEI4t6qE8XRSIvU2irka0dcpziKW6w=="],
"@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.15", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8XiKWbemmCbvNN0CLR9u3PQiet4gtEVIrX4zzLxnCj06AwsEDJwJVBbKrEI4t6qE8XRSIvU2irka0dcpziKW6w=="],
"@ai-sdk/react/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.15", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8XiKWbemmCbvNN0CLR9u3PQiet4gtEVIrX4zzLxnCj06AwsEDJwJVBbKrEI4t6qE8XRSIvU2irka0dcpziKW6w=="],
"@aklinker1/rollup-plugin-visualizer/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
"@aklinker1/rollup-plugin-visualizer/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
@@ -4863,6 +4886,8 @@
"accepts/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
"ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.15", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8XiKWbemmCbvNN0CLR9u3PQiet4gtEVIrX4zzLxnCj06AwsEDJwJVBbKrEI4t6qE8XRSIvU2irka0dcpziKW6w=="],
"antd/@ant-design/cssinjs": ["@ant-design/cssinjs@1.24.0", "", { "dependencies": { "@babel/runtime": "^7.11.1", "@emotion/hash": "^0.8.0", "@emotion/unitless": "^0.7.5", "classnames": "^2.3.1", "csstype": "^3.1.3", "rc-util": "^5.35.0", "stylis": "^4.3.4" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-K4cYrJBsgvL+IoozUXYjbT6LHHNt+19a9zkvpBPxLjFHas1UpPM2A5MlhROb0BT8N8WoavM5VsP9MeSeNK/3mg=="],
"antd/rc-collapse": ["rc-collapse@3.9.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA=="],

View File

@@ -57,7 +57,8 @@
"picocolors": "^1.1.1",
"rimraf": "^6.0.1",
"typedoc": "^0.28.15",
"typescript": "^5.9.2"
"typescript": "^5.9.2",
"yaml": "^2.8.2"
},
"trustedDependencies": [
"lefthook"
@@ -73,5 +74,8 @@
"overrides": {
"serialize-javascript": "7.0.3",
"lodash-es": "4.17.23"
},
"dependencies": {
"@wxt-dev/i18n": "^0.2.5"
}
}