Add i18n foundation and translations (#1027)
* feat: add i18n foundation * feat: localize sessions sidebar * Localize multirun/scheduled tasks and fix dialog dropdown interactions * localize git sidebar surface and add zh-CN keys * feat(ui): localize context panel, diff/plan views, and context sidebar content * fix(config): resolve user config home via fs/home before embedded home * localize header/chat UI and complete model/worktree panel strings * localize worktree + github issue/pr dialog flows * localize settings sections and split settings i18n dictionaries * localize additional settings sections and sidebars * localize more settings pages and dialogs * fix settings select trigger localization * localize tunnel settings ui surface * localize additional settings sections * localize keyboard shortcuts labels in settings * localize terminal and utility dialogs surfaces * feat(i18n): localize remaining UI strings * Add Ukrainian locale * Add Spanish locale * Add Brazilian Portuguese locale * Polish locale translations
This commit is contained in:
committed by
GitHub
parent
87db2ea210
commit
7d7285655d
@@ -66,6 +66,7 @@ import { ProjectActionsButton } from '@/components/layout/ProjectActionsButton';
|
||||
import { isDesktopShell, isVSCodeRuntime, startDesktopWindowDrag } from '@/lib/desktop';
|
||||
import { desktopHostsGet, locationMatchesHost, redactSensitiveUrl } from '@/lib/desktopHosts';
|
||||
import { resolveSessionDiffStats } from '@/components/session/sidebar/utils';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import type { Session } from '@opencode-ai/sdk/v2/client';
|
||||
|
||||
const DESKTOP_HEADER_ICON_BUTTON_CLASS = 'app-region-no-drag inline-flex h-8 w-8 items-center justify-center gap-2 rounded-md typography-ui-label font-medium text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50 hover:bg-interactive-hover transition-colors';
|
||||
@@ -132,6 +133,7 @@ const DesktopGitHubControl = React.memo(function DesktopGitHubControl({
|
||||
isSwitchingGitHubAccount,
|
||||
handleGitHubAccountSwitch,
|
||||
}: DesktopGitHubControlProps) {
|
||||
const { t } = useI18n();
|
||||
if (!githubAuthStatus?.connected || isMobile) {
|
||||
return null;
|
||||
}
|
||||
@@ -146,13 +148,13 @@ const DesktopGitHubControl = React.memo(function DesktopGitHubControl({
|
||||
DESKTOP_HEADER_ICON_BUTTON_CLASS,
|
||||
'h-7 w-7 overflow-hidden rounded-full border border-border/60 bg-muted/80 p-0'
|
||||
)}
|
||||
title={githubLogin ? `GitHub: ${githubLogin}` : 'GitHub connected'}
|
||||
title={githubLogin ? t('header.github.connectedWithLogin', { login: githubLogin }) : t('header.github.connected')}
|
||||
disabled={isSwitchingGitHubAccount}
|
||||
>
|
||||
{githubAvatarUrl ? (
|
||||
<img
|
||||
src={githubAvatarUrl}
|
||||
alt={githubLogin ? `${githubLogin} avatar` : 'GitHub avatar'}
|
||||
alt={githubLogin ? t('header.github.avatarWithLogin', { login: githubLogin }) : t('header.github.avatar')}
|
||||
className="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
referrerPolicy="no-referrer"
|
||||
@@ -164,7 +166,7 @@ const DesktopGitHubControl = React.memo(function DesktopGitHubControl({
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-64">
|
||||
<DropdownMenuLabel className="typography-ui-header font-semibold text-foreground">
|
||||
GitHub Accounts
|
||||
{t('header.github.accountsTitle')}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{githubAccounts.map((account) => {
|
||||
@@ -184,7 +186,7 @@ const DesktopGitHubControl = React.memo(function DesktopGitHubControl({
|
||||
{accountUser?.avatarUrl ? (
|
||||
<img
|
||||
src={accountUser.avatarUrl}
|
||||
alt={accountUser.login ? `${accountUser.login} avatar` : 'GitHub avatar'}
|
||||
alt={accountUser.login ? t('header.github.avatarWithLogin', { login: accountUser.login }) : t('header.github.avatar')}
|
||||
className="h-6 w-6 rounded-full border border-border/60 bg-muted object-cover"
|
||||
loading="lazy"
|
||||
referrerPolicy="no-referrer"
|
||||
@@ -216,12 +218,12 @@ const DesktopGitHubControl = React.memo(function DesktopGitHubControl({
|
||||
return (
|
||||
<div
|
||||
className="app-region-no-drag flex h-7 w-7 items-center justify-center overflow-hidden rounded-full border border-border/60 bg-muted/80"
|
||||
title={githubLogin ? `GitHub: ${githubLogin}` : 'GitHub connected'}
|
||||
title={githubLogin ? t('header.github.connectedWithLogin', { login: githubLogin }) : t('header.github.connected')}
|
||||
>
|
||||
{githubAvatarUrl ? (
|
||||
<img
|
||||
src={githubAvatarUrl}
|
||||
alt={githubLogin ? `${githubLogin} avatar` : 'GitHub avatar'}
|
||||
alt={githubLogin ? t('header.github.avatarWithLogin', { login: githubLogin }) : t('header.github.avatar')}
|
||||
className="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
referrerPolicy="no-referrer"
|
||||
@@ -284,6 +286,7 @@ const DesktopServicesMenu = React.memo(function DesktopServicesMenu({
|
||||
toggleFamilyExpanded,
|
||||
shortcutLabel,
|
||||
}: DesktopServicesMenuProps) {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<DropdownMenu
|
||||
open={isDesktopServicesOpen}
|
||||
@@ -303,8 +306,8 @@ const DesktopServicesMenu = React.memo(function DesktopServicesMenu({
|
||||
<button
|
||||
type="button"
|
||||
aria-label={isDesktopApp
|
||||
? `Open instance, usage and MCP (current: ${currentInstanceLabel})`
|
||||
: 'Open services, usage and MCP'}
|
||||
? t('header.services.openWithCurrent', { current: currentInstanceLabel })
|
||||
: t('header.services.open')}
|
||||
className={cn(
|
||||
DESKTOP_HEADER_ICON_BUTTON_CLASS,
|
||||
isDesktopApp ? 'w-auto max-w-[14rem] justify-start gap-1.5 px-2.5' : 'h-8 w-8'
|
||||
@@ -319,7 +322,16 @@ const DesktopServicesMenu = React.memo(function DesktopServicesMenu({
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
{isDesktopApp ? `Current instance: ${currentInstanceLabel}` : 'Services'} ({shortcutLabel('toggle_services_menu')}; next tab {shortcutLabel('cycle_services_tab')})
|
||||
{isDesktopApp
|
||||
? t('header.services.tooltip.currentInstanceWithShortcuts', {
|
||||
current: currentInstanceLabel,
|
||||
toggle: shortcutLabel('toggle_services_menu'),
|
||||
nextTab: shortcutLabel('cycle_services_tab'),
|
||||
})
|
||||
: t('header.services.tooltip.servicesWithShortcuts', {
|
||||
toggle: shortcutLabel('toggle_services_menu'),
|
||||
nextTab: shortcutLabel('cycle_services_tab'),
|
||||
})}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -365,7 +377,7 @@ const DesktopServicesMenu = React.memo(function DesktopServicesMenu({
|
||||
<div className="overflow-x-hidden">
|
||||
<div className="flex items-center justify-between gap-3 border-b border-[var(--interactive-border)] px-4 py-2.5">
|
||||
<div className="flex min-w-0 items-baseline gap-2">
|
||||
<span className="typography-ui-header font-semibold text-foreground">Rate limits</span>
|
||||
<span className="typography-ui-header font-semibold text-foreground">{t('header.services.rateLimits')}</span>
|
||||
<span className="truncate typography-micro text-muted-foreground">{formatTime(quotaLastUpdated)}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
@@ -389,7 +401,7 @@ const DesktopServicesMenu = React.memo(function DesktopServicesMenu({
|
||||
)}
|
||||
onClick={handleUsageRefresh}
|
||||
disabled={isQuotaLoading || isUsageRefreshSpinning}
|
||||
aria-label="Refresh rate limits"
|
||||
aria-label={t('header.services.refreshRateLimitsAria')}
|
||||
>
|
||||
<RiRefreshLine className={cn('h-4 w-4', isUsageRefreshSpinning && 'animate-spin')} />
|
||||
</button>
|
||||
@@ -398,7 +410,7 @@ const DesktopServicesMenu = React.memo(function DesktopServicesMenu({
|
||||
|
||||
{!hasRateLimits ? (
|
||||
<div className="px-4 py-5 text-center">
|
||||
<span className="typography-ui-label text-muted-foreground">No rate limits available.</span>
|
||||
<span className="typography-ui-label text-muted-foreground">{t('header.services.noRateLimits')}</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -414,7 +426,7 @@ const DesktopServicesMenu = React.memo(function DesktopServicesMenu({
|
||||
</div>
|
||||
{group.entries.length === 0 && (!group.modelFamilies || group.modelFamilies.length === 0) ? (
|
||||
<div className="px-4 pb-2">
|
||||
<span className="typography-ui-label text-muted-foreground">{group.error ?? 'No rate limits reported.'}</span>
|
||||
<span className="typography-ui-label text-muted-foreground">{group.error ?? t('header.services.noRateLimitsReported')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 px-4 pb-2">
|
||||
@@ -617,6 +629,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
rightDrawerOpen,
|
||||
desktopRightSidebarActionsHost = null,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const setSessionSwitcherOpen = useUIStore((state) => state.setSessionSwitcherOpen);
|
||||
const toggleSidebar = useUIStore((state) => state.toggleSidebar);
|
||||
const isSidebarOpen = useUIStore((state) => state.isSidebarOpen);
|
||||
@@ -904,7 +917,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
if (otherModels.length > 0) {
|
||||
group.modelFamilies.push({
|
||||
familyId: null,
|
||||
familyLabel: 'Other',
|
||||
familyLabel: t('header.services.modelFamily.other'),
|
||||
models: otherModels,
|
||||
});
|
||||
}
|
||||
@@ -1438,17 +1451,17 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
const tabs: TabConfig[] = React.useMemo(() => {
|
||||
if (isMobile) {
|
||||
const base: TabConfig[] = [
|
||||
{ id: 'chat', label: 'Chat', icon: RiChat4Line },
|
||||
{ id: 'chat', label: t('layout.mainTab.chat'), icon: RiChat4Line },
|
||||
];
|
||||
|
||||
if (showPlanTab) {
|
||||
base.push({ id: 'plan', label: 'Plan', icon: RiFileTextLine });
|
||||
base.push({ id: 'plan', label: t('layout.mainTab.plan'), icon: RiFileTextLine });
|
||||
}
|
||||
|
||||
base.push(
|
||||
{ id: 'diff', label: 'Diff', icon: 'diff' },
|
||||
{ id: 'files', label: 'Files', icon: RiFolder6Line },
|
||||
{ id: 'terminal', label: 'Terminal', icon: RiTerminalBoxLine },
|
||||
{ id: 'diff', label: t('layout.mainTab.diff'), icon: 'diff' },
|
||||
{ id: 'files', label: t('layout.mainTab.files'), icon: RiFolder6Line },
|
||||
{ id: 'terminal', label: t('layout.mainTab.terminal'), icon: RiTerminalBoxLine },
|
||||
);
|
||||
|
||||
return base;
|
||||
@@ -1456,7 +1469,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
|
||||
// Desktop: no tabs in header
|
||||
return [];
|
||||
}, [isMobile, showPlanTab]);
|
||||
}, [isMobile, showPlanTab, t]);
|
||||
|
||||
const shortcutLabel = React.useCallback((actionId: string) => {
|
||||
return formatShortcutForDisplay(getEffectiveShortcutCombo(actionId, shortcutOverrides));
|
||||
@@ -1471,14 +1484,14 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
const servicesTabs = React.useMemo(() => {
|
||||
const base: Array<{ value: 'instance' | 'usage' | 'mcp'; label: string; icon: RemixiconComponentType }> = [];
|
||||
if (isDesktopApp) {
|
||||
base.push({ value: 'instance', label: 'Instance', icon: RiServerLine });
|
||||
base.push({ value: 'instance', label: t('layout.services.instance'), icon: RiServerLine });
|
||||
}
|
||||
base.push(
|
||||
{ value: 'usage', label: 'Usage', icon: RiTimerLine },
|
||||
{ value: 'usage', label: t('layout.services.usage'), icon: RiTimerLine },
|
||||
{ value: 'mcp', label: 'MCP', icon: McpIcon as unknown as RemixiconComponentType }
|
||||
);
|
||||
return base;
|
||||
}, [isDesktopApp]);
|
||||
}, [isDesktopApp, t]);
|
||||
|
||||
const servicesTabItems = React.useMemo(() => {
|
||||
return servicesTabs.map((tab) => ({
|
||||
@@ -1490,10 +1503,10 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
|
||||
const quotaDisplayTabs = React.useMemo(() => {
|
||||
return [
|
||||
{ value: 'usage' as const, label: 'Used' },
|
||||
{ value: 'remaining' as const, label: 'Remaining' },
|
||||
{ value: 'usage' as const, label: t('header.services.used') },
|
||||
{ value: 'remaining' as const, label: t('header.services.remaining') },
|
||||
];
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
const quotaDisplayTabItems = React.useMemo(() => {
|
||||
return quotaDisplayTabs.map((tab) => ({ id: tab.value, label: tab.label }));
|
||||
@@ -1501,10 +1514,10 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
|
||||
const mobileServicesTabItems = React.useMemo<SortableTabsStripItem[]>(() => {
|
||||
return [
|
||||
{ id: 'usage', label: 'Usage', icon: <RiTimerLine className="h-3.5 w-3.5" /> },
|
||||
{ id: 'usage', label: t('layout.services.usage'), icon: <RiTimerLine className="h-3.5 w-3.5" /> },
|
||||
{ id: 'mcp', label: 'MCP', icon: <RiCommandLine className="h-3.5 w-3.5" /> },
|
||||
];
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
@@ -1633,17 +1646,17 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
{showPlanTab && (
|
||||
<Tooltip delayDuration={500}>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Open plan"
|
||||
onClick={handleOpenContextPlan}
|
||||
className={cn(desktopHeaderIconButtonClass, isContextPlanActive && 'bg-[var(--interactive-hover)]')}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('header.actions.openPlanAria')}
|
||||
onClick={handleOpenContextPlan}
|
||||
className={cn(desktopHeaderIconButtonClass, isContextPlanActive && 'bg-[var(--interactive-hover)]')}
|
||||
>
|
||||
<RiFileTextLine className="h-[18px] w-[18px]" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Plan ({shortcutLabel('toggle_context_plan')})</p>
|
||||
<p>{t('header.actions.planWithShortcut', { shortcut: shortcutLabel('toggle_context_plan') })}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
@@ -1674,14 +1687,14 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
shortcutLabel={shortcutLabel}
|
||||
/>
|
||||
<HeaderIconActionButton
|
||||
title={`Terminal panel (${shortcutLabel('toggle_terminal')})`}
|
||||
ariaLabel="Toggle terminal panel"
|
||||
title={t('header.actions.terminalPanelWithShortcut', { shortcut: shortcutLabel('toggle_terminal') })}
|
||||
ariaLabel={t('header.actions.toggleTerminalPanelAria')}
|
||||
onClick={toggleBottomTerminal}
|
||||
Icon={RiTerminalBoxLine}
|
||||
/>
|
||||
<HeaderIconActionButton
|
||||
title={`Right sidebar (${shortcutLabel('toggle_right_sidebar')})`}
|
||||
ariaLabel="Toggle right sidebar"
|
||||
title={t('header.actions.rightSidebarWithShortcut', { shortcut: shortcutLabel('toggle_right_sidebar') })}
|
||||
ariaLabel={t('header.actions.toggleRightSidebarAria')}
|
||||
onClick={toggleRightSidebar}
|
||||
Icon={RiLayoutRightLine}
|
||||
/>
|
||||
@@ -1709,12 +1722,12 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
)}
|
||||
style={webWindowControlsOverlayStyle}
|
||||
role="tablist"
|
||||
aria-label="Main navigation"
|
||||
aria-label={t('header.navigation.mainAria')}
|
||||
>
|
||||
<HeaderIconActionButton
|
||||
visible={!isSidebarOpen}
|
||||
title={`Open sessions (${shortcutLabel('toggle_sidebar')})`}
|
||||
ariaLabel="Open sessions"
|
||||
title={t('header.actions.openSessionsWithShortcut', { shortcut: shortcutLabel('toggle_sidebar') })}
|
||||
ariaLabel={t('header.actions.openSessionsAria')}
|
||||
onClick={handleOpenSessionSwitcher}
|
||||
className={`${desktopHeaderIconButtonClass} shrink-0`}
|
||||
Icon={RiLayoutLeftLine}
|
||||
@@ -1726,7 +1739,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="New session"
|
||||
aria-label={t('header.actions.newSessionAria')}
|
||||
onClick={handleHeaderNewSession}
|
||||
className={cn(desktopHeaderIconButtonClass, 'mr-6 shrink-0')}
|
||||
>
|
||||
@@ -1734,7 +1747,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>New session ({shortcutLabel('new_chat')})</p>
|
||||
<p>{t('header.actions.newSessionWithShortcut', { shortcut: shortcutLabel('new_chat') })}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
@@ -1827,7 +1840,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
mobileHeaderIconButtonClass,
|
||||
leftDrawerOpen && 'bg-interactive-selection text-interactive-selection-foreground'
|
||||
)}
|
||||
aria-label={leftDrawerOpen ? 'Close sessions' : 'Open sessions'}
|
||||
aria-label={leftDrawerOpen ? t('header.actions.closeSessionsAria') : t('header.actions.openSessionsAria')}
|
||||
>
|
||||
<RiLayoutLeftLine className="h-5 w-5" />
|
||||
</button>
|
||||
@@ -1836,7 +1849,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
type="button"
|
||||
onClick={() => setSessionSwitcherOpen(false)}
|
||||
className="app-region-no-drag h-9 w-9 p-2 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded-md active:bg-interactive-active"
|
||||
aria-label="Back"
|
||||
aria-label={t('header.actions.backAria')}
|
||||
>
|
||||
<RiArrowLeftSLine className="h-5 w-5" />
|
||||
</button>
|
||||
@@ -1845,14 +1858,14 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
type="button"
|
||||
onClick={handleOpenSessionSwitcher}
|
||||
className="app-region-no-drag h-9 w-9 p-2 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded-md active:bg-interactive-active"
|
||||
aria-label="Open sessions"
|
||||
aria-label={t('header.actions.openSessionsAria')}
|
||||
>
|
||||
<RiPlayListAddLine className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isSessionSwitcherOpen && (
|
||||
<span className="typography-ui-label font-semibold text-foreground">Sessions</span>
|
||||
<span className="typography-ui-label font-semibold text-foreground">{t('header.sessions.title')}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1865,7 +1878,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
<div
|
||||
className="flex items-center gap-0.5 rounded-lg bg-[var(--surface-muted)]/50 p-0.5"
|
||||
role="tablist"
|
||||
aria-label="Main navigation"
|
||||
aria-label={t('header.navigation.mainAria')}
|
||||
>
|
||||
{tabs.map((tab) => {
|
||||
const isActive = activeMainTab === tab.id;
|
||||
@@ -1904,7 +1917,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
{tab.showDot && (
|
||||
<span
|
||||
className="absolute top-1.5 right-1.5 h-2 w-2 rounded-full bg-primary"
|
||||
aria-label="Changes available"
|
||||
aria-label={t('header.changes.availableAria')}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
@@ -1946,7 +1959,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="View services"
|
||||
aria-label={t('header.services.viewAria')}
|
||||
className={mobileHeaderIconButtonClass}
|
||||
>
|
||||
<RiStackLine className="h-5 w-5" />
|
||||
@@ -1954,7 +1967,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Services</p>
|
||||
<p>{t('header.services.title')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent
|
||||
@@ -1987,7 +2000,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
type="button"
|
||||
onClick={() => setIsMobileRateLimitsOpen(false)}
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-interactive-hover"
|
||||
aria-label="Close services"
|
||||
aria-label={t('header.services.closeAria')}
|
||||
>
|
||||
<RiCloseLine className="h-5 w-5" />
|
||||
</button>
|
||||
@@ -2004,7 +2017,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
<div className="border-b border-[var(--interactive-border)]">
|
||||
<div className="flex items-center justify-between gap-3 px-4 py-3">
|
||||
<div className="flex flex-col min-w-0 gap-0.5">
|
||||
<span className="typography-ui-header font-semibold text-foreground">Rate limits</span>
|
||||
<span className="typography-ui-header font-semibold text-foreground">{t('header.services.rateLimits')}</span>
|
||||
<span className="truncate typography-micro text-muted-foreground">
|
||||
{formatTime(quotaLastUpdated)}
|
||||
</span>
|
||||
@@ -2021,7 +2034,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
Used
|
||||
{t('header.services.used')}
|
||||
</button>
|
||||
<span className="text-muted-foreground typography-ui-label px-0.5">·</span>
|
||||
<button
|
||||
@@ -2034,7 +2047,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
Remaining
|
||||
{t('header.services.remaining')}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
@@ -2046,7 +2059,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
)}
|
||||
onClick={handleUsageRefresh}
|
||||
disabled={isQuotaLoading || isUsageRefreshSpinning}
|
||||
aria-label="Refresh rate limits"
|
||||
aria-label={t('header.services.refreshRateLimitsAria')}
|
||||
>
|
||||
<RiRefreshLine className={cn('h-4 w-4', isUsageRefreshSpinning && 'animate-spin')} />
|
||||
</button>
|
||||
@@ -2056,7 +2069,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
|
||||
{!hasRateLimits && (
|
||||
<div className="px-4 py-6 text-center">
|
||||
<span className="typography-ui-label text-muted-foreground">No rate limits available.</span>
|
||||
<span className="typography-ui-label text-muted-foreground">{t('header.services.noRateLimits')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -2077,7 +2090,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
{group.entries.length === 0 && (!group.modelFamilies || group.modelFamilies.length === 0) ? (
|
||||
<div className="px-4 pb-2">
|
||||
<span className="typography-ui-label text-muted-foreground">
|
||||
{group.error ?? 'No rate limits reported.'}
|
||||
{group.error ?? t('header.services.noRateLimitsReported')}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user