Files
orca/src/renderer/src/components/github/github-rate-limit-display.tsx
T
JinjingandOrca 868b024dbe Fix localized strings getting stuck on language switch (#5150)
* Fix localized strings getting stuck on language switch

Replace static module-level translation evaluations with property
getters and i18n hooks to ensure UI elements update dynamically when
the active language changes.

- Convert static configuration properties to dynamic getters.
- Call `useTranslation()` to trigger re-renders on language updates.
- Recreate Tiptap editor extensions when the active language changes
  to refresh frozen placeholder configurations.

* Evaluate localized strings dynamically in static UI structures

- Use ES6 getters with `translate()` in static object maps (such as SSH
  connection status, usage ranges, and feature tour copy) to evaluate
  strings on demand when the language changes.
- Add the `useTranslation()` hook to `LinearIssueMarkdownToolbar` so it
  subscribes directly to translation updates.
- Extract markdown slash command primitives to a separate file to keep
  catalog configuration clean.

---------

Co-authored-by: Orca <help@stably.ai>
2026-06-11 19:45:50 -07:00

224 lines
7.0 KiB
TypeScript

import React, { useCallback, useEffect, useRef, useState } from 'react'
import { Gauge, RefreshCw } from 'lucide-react'
import { cn } from '@/lib/utils'
import { installWindowVisibilityInterval } from '@/lib/window-visibility-interval'
import { useAppStore } from '@/store'
import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client'
import type { GetRateLimitResult, GitHubRateLimitSnapshot } from '../../../../shared/types'
import { translate } from '@/i18n/i18n'
const REFRESH_INTERVAL_MS = 60_000
type BucketKey = 'core' | 'search' | 'graphql'
type BucketMeta = {
key: BucketKey
label: string
description: string
}
const BUCKETS: BucketMeta[] = [
{
key: 'core',
get label() {
return translate('auto.components.github.github.rate.limit.display.bb227706a6', 'REST')
},
get description() {
return translate('auto.components.github.github.rate.limit.display.c392c749a6', 'REST API')
}
},
{
key: 'search',
get label() {
return translate('auto.components.github.github.rate.limit.display.c377a4f06a', 'Search')
},
get description() {
return translate('auto.components.github.github.rate.limit.display.1f2f28a4de', 'Search API')
}
},
{
key: 'graphql',
get label() {
return translate('auto.components.github.github.rate.limit.display.1daf0f22a9', 'GraphQL')
},
get description() {
return translate('auto.components.github.github.rate.limit.display.01f7323e58', 'GraphQL API')
}
}
]
export function formatGitHubRateLimitReset(resetAt: number): string {
const deltaSec = Math.max(0, resetAt - Math.floor(Date.now() / 1000))
if (deltaSec < 60) {
return `${deltaSec}s`
}
const mins = Math.round(deltaSec / 60)
return `${mins}m`
}
export function toneForGitHubBucket(remaining: number, limit: number): 'ok' | 'warn' | 'crit' {
if (limit <= 0) {
return 'ok'
}
const pct = remaining / limit
if (pct < 0.1) {
return 'crit'
}
if (pct < 0.25) {
return 'warn'
}
return 'ok'
}
export function useGitHubRateLimitSnapshot(options?: { autoRefresh?: boolean }): {
snapshot: GitHubRateLimitSnapshot | null
hasError: boolean
isFetching: boolean
refresh: (force?: boolean) => Promise<void>
} {
const [snapshot, setSnapshot] = useState<GitHubRateLimitSnapshot | null>(null)
const [hasError, setHasError] = useState(false)
const [isFetching, setIsFetching] = useState(false)
const settings = useAppStore((s) => s.settings)
const latestToken = useRef(0)
const autoRefresh = options?.autoRefresh ?? true
const refresh = useCallback(
async (force = false): Promise<void> => {
const token = ++latestToken.current
setIsFetching(true)
try {
const target = getActiveRuntimeTarget(settings)
const params = force ? { force: true } : undefined
const res =
target.kind === 'environment'
? await callRuntimeRpc<GetRateLimitResult>(target, 'github.rateLimit', params ?? {}, {
timeoutMs: 30_000
})
: ((await window.api.gh.rateLimit(params)) as GetRateLimitResult | undefined)
if (token !== latestToken.current) {
return
}
if (res?.ok) {
setSnapshot(res.snapshot)
setHasError(false)
} else {
setHasError(true)
}
} catch {
if (token === latestToken.current) {
setHasError(true)
}
} finally {
if (token === latestToken.current) {
setIsFetching(false)
}
}
},
[settings]
)
useEffect(() => {
if (!autoRefresh) {
return
}
return installWindowVisibilityInterval({
run: () => void refresh(false),
intervalMs: REFRESH_INTERVAL_MS
})
}, [autoRefresh, refresh])
return { snapshot, hasError, isFetching, refresh }
}
function GitHubRateLimitRows({
snapshot
}: {
snapshot: GitHubRateLimitSnapshot
}): React.JSX.Element {
return (
<div className="flex flex-col gap-1 text-xs">
{BUCKETS.map((b) => {
const v = snapshot[b.key]
const tone = toneForGitHubBucket(v.remaining, v.limit)
return (
<div key={b.key} className="flex items-center justify-between gap-3">
<span className="text-muted-foreground">{b.description}</span>
<span
className={cn(
'tabular-nums text-foreground',
tone === 'crit' && 'text-red-600 dark:text-red-300',
tone === 'warn' && 'text-amber-700 dark:text-amber-300'
)}
>
{v.remaining}{' '}
{translate('auto.components.github.github.rate.limit.display.f42790d150', 'of')}
{v.limit}{' '}
{translate(
'auto.components.github.github.rate.limit.display.6da1858354',
'left · resets in'
)}
{formatGitHubRateLimitReset(v.resetAt)}
</span>
</div>
)
})}
</div>
)
}
export function GitHubRateLimitPanel({ className }: { className?: string }): React.JSX.Element {
const { snapshot, hasError, isFetching, refresh } = useGitHubRateLimitSnapshot()
return (
<div className={cn('space-y-3 rounded-md border border-border/60 p-3', className)}>
<div className="flex items-start justify-between gap-3">
<div className="space-y-0.5">
<div className="flex items-center gap-1.5 text-sm font-medium text-foreground">
<Gauge className="size-4" />
{translate(
'auto.components.github.github.rate.limit.display.58c5f88216',
'GitHub API Budget'
)}
</div>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.github.github.rate.limit.display.d5e5de9070',
'Orca uses REST, Search, and GraphQL through the GitHub CLI.'
)}
</p>
</div>
<button
type="button"
onClick={() => void refresh(true)}
disabled={isFetching}
className="inline-flex size-7 items-center justify-center rounded-md border border-border bg-secondary text-secondary-foreground transition hover:bg-accent disabled:opacity-50"
aria-label={translate(
'auto.components.github.github.rate.limit.display.d12d3d6f33',
'Refresh GitHub API budget'
)}
>
<RefreshCw className={cn('size-3.5', isFetching && 'animate-spin')} />
</button>
</div>
{hasError ? (
<div className="text-xs text-muted-foreground">
{translate(
'auto.components.github.github.rate.limit.display.34973d4695',
'GitHub API budget is unavailable.'
)}
</div>
) : snapshot ? (
<GitHubRateLimitRows snapshot={snapshot} />
) : (
<div className="text-xs text-muted-foreground">
{translate(
'auto.components.github.github.rate.limit.display.5509443543',
'Loading GitHub API budget…'
)}
</div>
)}
</div>
)
}