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:
Bohdan Triapitsyn
2026-04-26 14:03:39 +03:00
committed by GitHub
parent 87db2ea210
commit 7d7285655d
198 changed files with 24173 additions and 4365 deletions
@@ -63,6 +63,7 @@ import type {
GitHubPullRequestStatus,
GitRemote,
} from '@/lib/api/types';
import { useI18n } from '@/lib/i18n';
type MergeMethod = 'merge' | 'squash' | 'rebase';
@@ -278,6 +279,7 @@ export const PullRequestSection: React.FC<{
remoteBranches?: string[];
onGeneratedDescription?: () => void;
}> = ({ directory, branch, baseBranch, trackingBranch, remotes = [], remoteBranches = [], onGeneratedDescription }) => {
const { t } = useI18n();
const { github } = useRuntimeAPIs();
const githubAuthStatus = useGitHubAuthStore((state) => state.status);
const githubAuthChecked = useGitHubAuthStore((state) => state.hasChecked);
@@ -516,7 +518,7 @@ export const PullRequestSection: React.FC<{
const openChecksDialog = React.useCallback(async () => {
if (!github?.prContext) {
toast.error('GitHub runtime API unavailable');
toast.error(t('gitView.pr.toast.githubApiUnavailable'));
return;
}
if (!pr) return;
@@ -532,7 +534,7 @@ export const PullRequestSection: React.FC<{
setCheckDetails(ctx);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
toast.error('Failed to load check details', { description: message });
toast.error(t('gitView.pr.toast.loadCheckDetailsFailed'), { description: message });
} finally {
setIsLoadingCheckDetails(false);
}
@@ -540,7 +542,7 @@ export const PullRequestSection: React.FC<{
const openCommentsDialog = React.useCallback(async () => {
if (!github?.prContext) {
toast.error('GitHub runtime API unavailable');
toast.error(t('gitView.pr.toast.githubApiUnavailable'));
return;
}
if (!pr) return;
@@ -555,7 +557,7 @@ export const PullRequestSection: React.FC<{
setCommentsDetails(ctx);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
toast.error('Failed to load comments', { description: message });
toast.error(t('gitView.pr.toast.loadCommentsFailed'), { description: message });
} finally {
setIsLoadingCommentsDetails(false);
}
@@ -594,11 +596,11 @@ export const PullRequestSection: React.FC<{
const issue = (commentsDetails?.issueComments ?? []).map((comment) => ({
id: `issue-${comment.id}`,
body: comment.body || '',
authorName: comment.author?.name || comment.author?.login || 'Unknown author',
authorName: comment.author?.name || comment.author?.login || t('gitView.pr.comments.unknownAuthor'),
authorLogin: comment.author?.login || null,
avatarUrl: comment.author?.avatarUrl || null,
createdAt: comment.createdAt,
context: 'General comment',
context: t('gitView.pr.comments.generalContext'),
path: null as string | null,
line: null as number | null,
}));
@@ -606,11 +608,11 @@ export const PullRequestSection: React.FC<{
const review = (commentsDetails?.reviewComments ?? []).map((comment) => ({
id: `review-${comment.id}`,
body: comment.body || '',
authorName: comment.author?.name || comment.author?.login || 'Unknown author',
authorName: comment.author?.name || comment.author?.login || t('gitView.pr.comments.unknownAuthor'),
authorLogin: comment.author?.login || null,
avatarUrl: comment.author?.avatarUrl || null,
createdAt: comment.createdAt,
context: 'Code review comment',
context: t('gitView.pr.comments.reviewContext'),
path: comment.path || null,
line: comment.line ?? null,
}));
@@ -624,11 +626,11 @@ export const PullRequestSection: React.FC<{
return aVal - bVal;
});
return all;
}, [commentsDetails]);
}, [commentsDetails, t]);
const resolveChatDispatchTarget = React.useCallback((): ChatDispatchTarget | null => {
if (!currentSessionId) {
toast.error('No active session', { description: 'Open a chat session first.' });
toast.error(t('gitView.pr.toast.noActiveSession'), { description: t('gitView.pr.toast.noActiveSessionDescription') });
return null;
}
@@ -637,7 +639,7 @@ export const PullRequestSection: React.FC<{
const providerID = currentProviderId || lastUsedProvider?.providerID;
const modelID = currentModelId || lastUsedProvider?.modelID;
if (!providerID || !modelID) {
toast.error('No model selected');
toast.error(t('gitView.pr.toast.noModelSelected'));
return null;
}
@@ -670,7 +672,7 @@ export const PullRequestSection: React.FC<{
target.currentVariant ?? undefined,
).catch((e) => {
const message = e instanceof Error ? e.message : String(e);
toast.error('Failed to send message', { description: message });
toast.error(t('gitView.pr.toast.sendMessageFailed'), { description: message });
});
}, []);
@@ -743,7 +745,7 @@ export const PullRequestSection: React.FC<{
{run.job?.steps && run.job.steps.length > 0 ? (
<div className="space-y-1">
<div className="typography-micro text-muted-foreground">Steps</div>
<div className="typography-micro text-muted-foreground">{t('gitView.pr.checks.steps')}</div>
<div className="space-y-1">
{run.job.steps.map((step, idx) => {
const c = (step.conclusion || '').toLowerCase();
@@ -787,11 +789,11 @@ export const PullRequestSection: React.FC<{
</button>
<CollapsibleContent>
<div className="ml-6 mt-1 rounded border border-border/40 bg-transparent px-2 py-2 typography-micro text-muted-foreground space-y-1">
{typeof step.number === 'number' ? <div>Step: {step.number}</div> : null}
{step.status ? <div>Status: {step.status}</div> : null}
{step.conclusion ? <div>Conclusion: {step.conclusion}</div> : null}
{step.startedAt ? <div>Started: {formatTimestamp(step.startedAt)}</div> : null}
{step.completedAt ? <div>Completed: {formatTimestamp(step.completedAt)}</div> : null}
{typeof step.number === 'number' ? <div>{t('gitView.pr.checks.stepLabel')}: {step.number}</div> : null}
{step.status ? <div>{t('gitView.pr.checks.statusLabel')}: {step.status}</div> : null}
{step.conclusion ? <div>{t('gitView.pr.checks.conclusionLabel')}: {step.conclusion}</div> : null}
{step.startedAt ? <div>{t('gitView.pr.checks.startedLabel')}: {formatTimestamp(step.startedAt)}</div> : null}
{step.completedAt ? <div>{t('gitView.pr.checks.completedLabel')}: {formatTimestamp(step.completedAt)}</div> : null}
</div>
</CollapsibleContent>
</Collapsible>
@@ -808,7 +810,7 @@ export const PullRequestSection: React.FC<{
setActiveMainTab('chat');
if (!github?.prContext) {
toast.error('GitHub runtime API unavailable');
toast.error(t('gitView.pr.toast.githubApiUnavailable'));
return;
}
if (!directory || !pr) return;
@@ -827,7 +829,7 @@ export const PullRequestSection: React.FC<{
});
if (failed.length === 0) {
toast.message('No failed checks');
toast.message(t('gitView.pr.toast.noFailedChecks'));
return;
}
@@ -856,7 +858,7 @@ export const PullRequestSection: React.FC<{
dispatchSyntheticPrompt(target, visibleText, instructionsText, payloadText);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
toast.error('Failed to load checks', { description: message });
toast.error(t('gitView.pr.toast.loadChecksFailed'), { description: message });
}
}, [directory, dispatchSyntheticPrompt, github, pr, resolveChatDispatchTarget, setActiveMainTab]);
@@ -864,7 +866,7 @@ export const PullRequestSection: React.FC<{
setActiveMainTab('chat');
if (!github?.prContext) {
toast.error('GitHub runtime API unavailable');
toast.error(t('gitView.pr.toast.githubApiUnavailable'));
return;
}
if (!directory || !pr) return;
@@ -879,7 +881,7 @@ export const PullRequestSection: React.FC<{
const reviewComments = context.reviewComments ?? [];
const total = issueComments.length + reviewComments.length;
if (total === 0) {
toast.message('No PR comments');
toast.message(t('gitView.pr.toast.noPrComments'));
return;
}
@@ -895,7 +897,7 @@ export const PullRequestSection: React.FC<{
dispatchSyntheticPrompt(target, visibleText, instructionsText, payloadText);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
toast.error('Failed to load PR comments', { description: message });
toast.error(t('gitView.pr.toast.loadPrCommentsFailed'), { description: message });
}
}, [directory, dispatchSyntheticPrompt, github, pr, resolveChatDispatchTarget, setActiveMainTab]);
@@ -1136,7 +1138,7 @@ export const PullRequestSection: React.FC<{
onGeneratedDescription?.();
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
toast.error('Failed to generate description', { description: message });
toast.error(t('gitView.pr.toast.generateDescriptionFailed'), { description: message });
} finally {
setIsGenerating(false);
}
@@ -1144,22 +1146,22 @@ export const PullRequestSection: React.FC<{
const createPr = React.useCallback(async () => {
if (!github?.prCreate) {
toast.error('GitHub runtime API unavailable');
toast.error(t('gitView.pr.toast.githubApiUnavailable'));
return;
}
const trimmedTitle = title.trim();
if (!trimmedTitle) {
toast.error('Title is required');
toast.error(t('gitView.pr.toast.titleRequired'));
return;
}
const trimmedBase = targetBaseBranch.trim();
if (!trimmedBase) {
toast.error('Base branch is required');
toast.error(t('gitView.pr.toast.baseBranchRequired'));
return;
}
if (trimmedBase === branch) {
toast.error('Base branch must differ from head branch');
toast.error(t('gitView.pr.toast.baseMustDifferFromHead'));
return;
}
@@ -1176,13 +1178,13 @@ export const PullRequestSection: React.FC<{
draft,
...(selectedRemote ? { remote: selectedRemote.name } : {}),
});
toast.success('PR created');
toast.success(t('gitView.pr.toast.prCreated'));
updatePrStatus(prStatusKey, (prev) => (prev ? { ...prev, pr } : prev));
await refresh({ force: true });
scheduleActionRefresh();
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
toast.error('Failed to create PR', { description: message });
toast.error(t('gitView.pr.toast.createPrFailed'), { description: message });
} finally {
setIsCreating(false);
}
@@ -1190,22 +1192,22 @@ export const PullRequestSection: React.FC<{
const mergePr = React.useCallback(async (pr: GitHubPullRequest) => {
if (!github?.prMerge) {
toast.error('GitHub runtime API unavailable');
toast.error(t('gitView.pr.toast.githubApiUnavailable'));
return;
}
setIsMerging(true);
try {
const result = await github.prMerge({ directory, number: pr.number, method: mergeMethod });
if (result.merged) {
toast.success('PR merged');
toast.success(t('gitView.pr.toast.prMerged'));
} else {
toast.message('PR not merged', { description: result.message || 'Not mergeable' });
toast.message(t('gitView.pr.toast.prNotMerged'), { description: result.message || t('gitView.pr.notMergeable') });
}
await refresh({ force: true });
scheduleActionRefresh();
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
toast.error('Merge failed', { description: message });
toast.error(t('gitView.pr.toast.mergeFailed'), { description: message });
if (pr.url) {
void openExternal(pr.url);
}
@@ -1216,18 +1218,18 @@ export const PullRequestSection: React.FC<{
const markReady = React.useCallback(async (pr: GitHubPullRequest) => {
if (!github?.prReady) {
toast.error('GitHub runtime API unavailable');
toast.error(t('gitView.pr.toast.githubApiUnavailable'));
return;
}
setIsMarkingReady(true);
try {
await github.prReady({ directory, number: pr.number });
toast.success('Marked ready for review');
toast.success(t('gitView.pr.toast.markedReady'));
await refresh({ force: true });
scheduleActionRefresh();
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
toast.error('Failed to mark ready', { description: message });
toast.error(t('gitView.pr.toast.markReadyFailed'), { description: message });
if (pr.url) {
void openExternal(pr.url);
}
@@ -1238,13 +1240,13 @@ export const PullRequestSection: React.FC<{
const updatePr = React.useCallback(async (pr: GitHubPullRequest) => {
if (!github?.prUpdate) {
toast.error('GitHub runtime API unavailable');
toast.error(t('gitView.pr.toast.githubApiUnavailable'));
return;
}
const trimmedTitle = editTitle.trim();
if (!trimmedTitle) {
toast.error('Title is required');
toast.error(t('gitView.pr.toast.titleRequired'));
return;
}
@@ -1266,12 +1268,12 @@ export const PullRequestSection: React.FC<{
}
: prev));
setIsEditingPr(false);
toast.success('PR updated');
toast.success(t('gitView.pr.toast.prUpdated'));
await refresh({ force: true });
scheduleActionRefresh();
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
toast.error('Failed to update PR', { description: message });
toast.error(t('gitView.pr.toast.updatePrFailed'), { description: message });
} finally {
setIsUpdating(false);
}
@@ -1312,17 +1314,17 @@ export const PullRequestSection: React.FC<{
type="button"
className="inline-flex size-6 shrink-0 items-center justify-center rounded-md border border-border/60 bg-background/70 hover:bg-interactive-hover/60"
onClick={() => void openExternal(pr.url)}
aria-label="Open PR on GitHub"
aria-label={t('gitView.pr.actions.openOnGitHubAria')}
>
<PrStateIcon className="size-4 shrink-0" style={{ color: prColorVar }} />
</button>
</TooltipTrigger>
<TooltipContent><p>Open PR on GitHub</p></TooltipContent>
<TooltipContent><p>{t('gitView.pr.actions.openOnGitHub')}</p></TooltipContent>
</Tooltip>
) : (
<PrStateIcon className="size-4 shrink-0" style={{ color: 'var(--surface-muted-foreground)' }} />
)}
<h3 className="typography-ui-header font-semibold text-foreground truncate">Pull Request</h3>
<h3 className="typography-ui-header font-semibold text-foreground truncate">{t('gitView.pullRequest.title')}</h3>
{pr ? (
<span className="typography-meta text-muted-foreground truncate">#{pr.number}</span>
) : null}
@@ -1372,7 +1374,7 @@ export const PullRequestSection: React.FC<{
<span style={{ color: prColorVar }}>
{pr.state}{pr.draft ? ' (draft)' : ''}
</span>
{pr.mergeable === false ? ' · not mergeable' : ''}
{pr.mergeable === false ? ` · ${t('gitView.pr.notMergeable')}` : ''}
{pr.state === 'open' && typeof pr.mergeableState === 'string' && pr.mergeableState && pr.mergeableState !== 'unknown'
? ` · ${pr.mergeableState}`
: ''}
@@ -1383,18 +1385,18 @@ export const PullRequestSection: React.FC<{
<div className={bodyClassName}>
{shouldShowConnectionNotice ? (
<div className="space-y-2">
<div className="typography-meta text-muted-foreground">
GitHub not connected. Connect your GitHub account in settings.
<div className="typography-meta text-muted-foreground">
{t('gitView.pr.githubNotConnected')}
</div>
<Button variant="outline" size="sm" onClick={openGitHubSettings} className="w-fit">
Open settings
{t('gitView.pr.actions.openSettings')}
</Button>
</div>
) : null}
{error ? (
<div className="space-y-2">
<div className="typography-ui-label text-foreground">PR status unavailable</div>
<div className="typography-ui-label text-foreground">{t('gitView.pr.statusUnavailable')}</div>
<div className="typography-meta text-muted-foreground break-words">{error}</div>
{repoUrl ? (
<Button variant="outline" size="sm" asChild className="w-fit">
@@ -1410,7 +1412,7 @@ export const PullRequestSection: React.FC<{
{!pr && !isInitialStatusResolved && !error && !shouldShowConnectionNotice ? (
<div className="flex items-center gap-2 typography-micro text-muted-foreground">
<RiLoader4Line className="size-4 animate-spin" />
Checking PR status...
{t('gitView.pr.checkingStatus')}
</div>
) : pr ? (
<div className="flex flex-col gap-2">
@@ -1421,7 +1423,7 @@ export const PullRequestSection: React.FC<{
<Input
value={editTitle}
onChange={(e) => setEditTitle(e.target.value)}
placeholder="PR title"
placeholder={t('gitView.pr.placeholder.title')}
autoCorrect={hasTouchInput ? "on" : "off"}
autoCapitalize={hasTouchInput ? "sentences" : "off"}
spellCheck={hasTouchInput}
@@ -1430,7 +1432,7 @@ export const PullRequestSection: React.FC<{
value={editBody}
onChange={(e) => setEditBody(e.target.value)}
className="min-h-[120px] bg-background/80"
placeholder="Describe this PR"
placeholder={t('gitView.pr.placeholder.description')}
autoCorrect={hasTouchInput ? "on" : "off"}
autoCapitalize={hasTouchInput ? "sentences" : "off"}
spellCheck={hasTouchInput}
@@ -1446,18 +1448,18 @@ export const PullRequestSection: React.FC<{
/>
) : (
<div className="typography-micro text-muted-foreground whitespace-pre-wrap break-words mt-1">
{isHydratingCurrentPrBody ? 'Loading description...' : 'No description provided.'}
{isHydratingCurrentPrBody ? t('gitView.pr.loadingDescription') : t('gitView.pr.noDescription')}
</div>
)}
</>
)}
{canMerge && pr.draft ? (
<div className="typography-micro text-muted-foreground">
Draft PRs must be marked ready before merge.
{t('gitView.pr.draftMustBeReady')}
</div>
) : null}
{!canMerge ? (
<div className="typography-micro text-muted-foreground">No merge permission; use Open in GitHub.</div>
<div className="typography-micro text-muted-foreground">{t('gitView.pr.noMergePermission')}</div>
) : null}
</div>
@@ -1478,12 +1480,12 @@ export const PullRequestSection: React.FC<{
setEditBody(pr.body || '');
}}
disabled={isUpdating}
aria-label="Cancel editing"
aria-label={t('gitView.pr.actions.cancelEditingAria')}
>
<RiCloseLine className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent><p>Cancel editing</p></TooltipContent>
<TooltipContent><p>{t('gitView.pr.actions.cancelEditing')}</p></TooltipContent>
</Tooltip>
<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
@@ -1492,12 +1494,12 @@ export const PullRequestSection: React.FC<{
className="h-7 w-7 px-0"
onClick={() => updatePr(pr)}
disabled={isUpdating || !editTitle.trim()}
aria-label="Save PR title and description"
aria-label={t('gitView.pr.actions.savePrAria')}
>
{isUpdating ? <RiLoader4Line className="size-4 animate-spin" /> : <RiCheckLine className="size-4" />}
</Button>
</TooltipTrigger>
<TooltipContent><p>Save PR title and description</p></TooltipContent>
<TooltipContent><p>{t('gitView.pr.actions.savePr')}</p></TooltipContent>
</Tooltip>
</>
) : (
@@ -1508,12 +1510,12 @@ export const PullRequestSection: React.FC<{
size="sm"
className="h-7 w-7 px-0"
onClick={() => setIsEditingPr(true)}
aria-label="Edit PR title and description"
aria-label={t('gitView.pr.actions.editPrAria')}
>
<RiEditLine className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent><p>Edit PR title and description</p></TooltipContent>
<TooltipContent><p>{t('gitView.pr.actions.editPr')}</p></TooltipContent>
</Tooltip>
)
) : null}
@@ -1527,12 +1529,12 @@ export const PullRequestSection: React.FC<{
className="h-7 w-7 px-0"
onClick={openChecksDialog}
disabled={isLoadingCheckDetails}
aria-label="Open checks details"
aria-label={t('gitView.pr.actions.openChecksAria')}
>
{isLoadingCheckDetails ? <RiLoader4Line className="size-4 animate-spin" /> : <RiInformationLine className="size-4" />}
</Button>
</TooltipTrigger>
<TooltipContent><p>Open checks details</p></TooltipContent>
<TooltipContent><p>{t('gitView.pr.actions.openChecks')}</p></TooltipContent>
</Tooltip>
) : null}
@@ -1544,12 +1546,12 @@ export const PullRequestSection: React.FC<{
size="sm"
className="h-7 w-7 px-0 border-[var(--status-success-border)] bg-[var(--status-success-background)] text-[var(--status-success)]"
onClick={sendFailedChecksToChat}
aria-label="Resolve failed checks with agent"
aria-label={t('gitView.pr.actions.resolveFailedChecksAria')}
>
<RiErrorWarningLine className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent><p>Resolve failed checks with agent</p></TooltipContent>
<TooltipContent><p>{t('gitView.pr.actions.resolveFailedChecks')}</p></TooltipContent>
</Tooltip>
) : null}
@@ -1560,12 +1562,12 @@ export const PullRequestSection: React.FC<{
size="sm"
className="h-7 w-7 px-0"
onClick={openCommentsDialog}
aria-label="Open PR comments"
aria-label={t('gitView.pr.actions.openCommentsAria')}
>
<RiChat4Line className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent><p>Open PR comments</p></TooltipContent>
<TooltipContent><p>{t('gitView.pr.actions.openComments')}</p></TooltipContent>
</Tooltip>
<Tooltip delayDuration={300}>
@@ -1575,12 +1577,12 @@ export const PullRequestSection: React.FC<{
size="sm"
className="h-7 w-7 px-0 border-[var(--status-success-border)] bg-[var(--status-success-background)] text-[var(--status-success)]"
onClick={sendCommentsToChat}
aria-label="Share comments with agent"
aria-label={t('gitView.pr.actions.shareCommentsAria')}
>
<RiAiGenerate2 className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent><p>Share comments with agent</p></TooltipContent>
<TooltipContent><p>{t('gitView.pr.actions.shareComments')}</p></TooltipContent>
</Tooltip>
{canMerge && pr.draft && pr.state === 'open' ? (
@@ -1592,12 +1594,12 @@ export const PullRequestSection: React.FC<{
className="h-7 w-7 px-0"
onClick={() => markReady(pr)}
disabled={isMarkingReady || isMerging || isUpdating || isEditingPr}
aria-label="Mark PR ready for review"
aria-label={t('gitView.pr.actions.markReadyAria')}
>
{isMarkingReady ? <RiLoader4Line className="size-4 animate-spin" /> : <RiCheckboxCircleLine className="size-4" />}
</Button>
</TooltipTrigger>
<TooltipContent><p>Mark PR ready for review</p></TooltipContent>
<TooltipContent><p>{t('gitView.pr.actions.markReady')}</p></TooltipContent>
</Tooltip>
) : null}
</div>
@@ -1614,9 +1616,9 @@ export const PullRequestSection: React.FC<{
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="squash">Squash</SelectItem>
<SelectItem value="merge">Merge</SelectItem>
<SelectItem value="rebase">Rebase</SelectItem>
<SelectItem value="squash">{t('gitView.pr.mergeMethod.squash')}</SelectItem>
<SelectItem value="merge">{t('gitView.pr.mergeMethod.merge')}</SelectItem>
<SelectItem value="rebase">{t('gitView.pr.mergeMethod.rebase')}</SelectItem>
</SelectContent>
</Select>
<Tooltip delayDuration={300}>
@@ -1626,12 +1628,12 @@ export const PullRequestSection: React.FC<{
className="h-7 w-7 px-0"
onClick={() => mergePr(pr)}
disabled={isMerging || isMarkingReady || pr.state !== 'open' || pr.draft || isUpdating || isEditingPr}
aria-label="Merge pull request"
aria-label={t('gitView.pr.actions.mergePrAria')}
>
{isMerging ? <RiLoader4Line className="size-4 animate-spin" /> : <RiGitMergeLine className="size-4" />}
</Button>
</TooltipTrigger>
<TooltipContent><p>Merge pull request</p></TooltipContent>
<TooltipContent><p>{t('gitView.pr.actions.mergePr')}</p></TooltipContent>
</Tooltip>
</>
) : null}
@@ -1643,7 +1645,7 @@ export const PullRequestSection: React.FC<{
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between gap-2">
<div className="min-w-0">
<div className="typography-ui-label text-foreground">Create PR</div>
<div className="typography-ui-label text-foreground">{t('gitView.pr.createTitle')}</div>
<div className="typography-micro text-muted-foreground truncate">
{branch} {targetBaseBranch}
</div>
@@ -1652,18 +1654,18 @@ export const PullRequestSection: React.FC<{
<Button variant="outline" size="sm" asChild>
<a href={repoUrl} target="_blank" rel="noopener noreferrer">
<RiExternalLinkLine className="size-4" />
Repo
{t('gitView.pr.actions.repo')}
</a>
</Button>
) : null}
</div>
<label className="space-y-1">
<div className="typography-micro text-muted-foreground">Title</div>
<div className="typography-micro text-muted-foreground">{t('gitView.pr.field.title')}</div>
<Input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="PR title"
placeholder={t('gitView.pr.placeholder.title')}
autoCorrect={hasTouchInput ? "on" : "off"}
autoCapitalize={hasTouchInput ? "sentences" : "off"}
spellCheck={hasTouchInput}
@@ -1671,11 +1673,11 @@ export const PullRequestSection: React.FC<{
</label>
<label className="space-y-1">
<div className="typography-micro text-muted-foreground">Base branch</div>
<div className="typography-micro text-muted-foreground">{t('gitView.pr.field.baseBranch')}</div>
{availableBaseBranches.length > 0 ? (
<Select value={targetBaseBranch} onValueChange={setTargetBaseBranch}>
<SelectTrigger className="h-9">
<SelectValue placeholder="Select base branch" />
<SelectValue placeholder={t('gitView.pr.placeholder.selectBaseBranch')} />
</SelectTrigger>
<SelectContent>
{availableBaseBranches.map((candidate) => (
@@ -1687,18 +1689,18 @@ export const PullRequestSection: React.FC<{
<Input
value={targetBaseBranch}
onChange={(e) => setTargetBaseBranch(e.target.value)}
placeholder="main"
placeholder={t('gitView.pr.placeholder.main')}
/>
)}
</label>
<label className="space-y-1">
<div className="typography-micro text-muted-foreground">Description</div>
<div className="typography-micro text-muted-foreground">{t('gitView.pr.field.description')}</div>
<Textarea
value={body}
onChange={(e) => setBody(e.target.value)}
className="min-h-[110px]"
placeholder="What changed and why"
placeholder={t('gitView.pr.placeholder.whatChanged')}
autoCorrect={hasTouchInput ? "on" : "off"}
autoCapitalize={hasTouchInput ? "sentences" : "off"}
spellCheck={hasTouchInput}
@@ -1722,9 +1724,9 @@ export const PullRequestSection: React.FC<{
size="sm"
checked={draft}
onChange={(next) => setDraft(next)}
ariaLabel="Toggle draft PR"
ariaLabel={t('gitView.pr.actions.toggleDraftAria')}
/>
<span className="typography-ui-label text-foreground select-none">Draft</span>
<span className="typography-ui-label text-foreground select-none">{t('gitView.pr.field.draft')}</span>
</div>
{/* Additional Context Section */}
@@ -1732,20 +1734,20 @@ export const PullRequestSection: React.FC<{
<div className="space-y-2">
<div className="flex items-center justify-between gap-2">
<span className="typography-micro text-muted-foreground">
Additional context (optional)
{t('gitView.pr.additionalContext.optional')}
</span>
<Button
variant="outline"
size="sm"
onClick={() => setIsContextSheetOpen(true)}
>
{additionalContext.trim() ? 'Edit' : 'Add'}
{additionalContext.trim() ? t('gitView.pr.actions.edit') : t('gitView.pr.actions.add')}
</Button>
</div>
{additionalContext.trim() && (
<div className="flex items-center gap-2">
<span className="inline-flex items-center rounded-full bg-[var(--interactive-selection)] px-2 py-0.5 text-xs text-[var(--interactive-selection-foreground)]">
Context added
{t('gitView.pr.additionalContext.added')}
</span>
</div>
)}
@@ -1754,10 +1756,10 @@ export const PullRequestSection: React.FC<{
<Collapsible open={isContextOpen} onOpenChange={setIsContextOpen}>
<CollapsibleTrigger className="flex w-full items-center justify-between rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-elevated)] px-3 py-2 hover:bg-[var(--interactive-hover)]">
<span className="typography-micro text-muted-foreground">
Additional context (optional)
{t('gitView.pr.additionalContext.optional')}
</span>
<span className="typography-micro text-[var(--primary-base)]">
{isContextOpen ? 'Hide' : additionalContext.trim() ? 'Edit' : 'Add'}
{isContextOpen ? t('gitView.pr.actions.hide') : additionalContext.trim() ? t('gitView.pr.actions.edit') : t('gitView.pr.actions.add')}
</span>
</CollapsibleTrigger>
<CollapsibleContent>
@@ -1766,10 +1768,10 @@ export const PullRequestSection: React.FC<{
value={additionalContext}
onChange={(e) => setAdditionalContext(e.target.value)}
className="min-h-[100px] bg-transparent"
placeholder="Explain why this change is needed...&#10;Mention how to test (commands / steps)...&#10;Call out risks / rollout plan..."
placeholder={t('gitView.pr.placeholder.additionalContext')}
/>
<p className="typography-micro text-muted-foreground">
This text is only used to guide PR generation.
{t('gitView.pr.additionalContext.hint')}
</p>
</div>
</CollapsibleContent>
@@ -1780,14 +1782,14 @@ export const PullRequestSection: React.FC<{
<MobileOverlayPanel
open={isContextSheetOpen}
onClose={() => setIsContextSheetOpen(false)}
title="Additional context"
title={t('gitView.pr.additionalContext.title')}
footer={
<Button
size="sm"
onClick={() => setIsContextSheetOpen(false)}
className="w-full"
>
Done
{t('gitView.common.done')}
</Button>
}
>
@@ -1796,11 +1798,11 @@ export const PullRequestSection: React.FC<{
value={additionalContext}
onChange={(e) => setAdditionalContext(e.target.value)}
className="min-h-[200px] bg-transparent"
placeholder="Explain why this change is needed...&#10;Mention how to test (commands / steps)...&#10;Call out risks / rollout plan..."
placeholder={t('gitView.pr.placeholder.additionalContext')}
autoFocus
/>
<p className="typography-micro text-muted-foreground">
This text is only used to guide PR generation.
{t('gitView.pr.additionalContext.hint')}
</p>
</div>
</MobileOverlayPanel>
@@ -1813,7 +1815,7 @@ export const PullRequestSection: React.FC<{
disabled={isGenerating || isCreating}
>
{isGenerating ? <RiLoader4Line className="size-4 animate-spin" /> : <RiAiGenerate2 className="size-4 text-primary" />}
Generate
{t('gitView.commit.generate')}
</Button>
<div className="flex-1" />
<Button
@@ -1825,7 +1827,7 @@ export const PullRequestSection: React.FC<{
<span className="inline-flex size-4 items-center justify-center">
{isCreating ? <RiLoader4Line className="size-4 animate-spin" /> : <RiGitPullRequestLine className="size-4" />}
</span>
<span>Create PR</span>
<span>{t('gitView.pr.actions.createPr')}</span>
</Button>
</div>
</div>
@@ -1837,10 +1839,10 @@ export const PullRequestSection: React.FC<{
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<RiGitPullRequestLine className="h-5 w-5" />
Check Details
{t('gitView.pr.checkDetails.title')}
</DialogTitle>
<DialogDescription>
{pr ? `PR #${pr.number}` : 'Pull request'}
{pr ? t('gitView.pr.numberLabel', { number: pr.number }) : t('gitView.pullRequest.title')}
</DialogDescription>
</DialogHeader>
@@ -1848,7 +1850,7 @@ export const PullRequestSection: React.FC<{
{isLoadingCheckDetails ? (
<div className="text-center text-muted-foreground py-8 flex items-center justify-center gap-2">
<RiLoader4Line className="h-4 w-4 animate-spin" />
Loading...
{t('gitView.loading.loading')}
</div>
) : null}
@@ -1864,7 +1866,7 @@ export const PullRequestSection: React.FC<{
);
})
) : (
<div className="text-center text-muted-foreground py-8">No check details available.</div>
<div className="text-center text-muted-foreground py-8">{t('gitView.pr.checkDetails.empty')}</div>
)}
</div>
) : null}
@@ -1878,9 +1880,9 @@ export const PullRequestSection: React.FC<{
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<RiGitPullRequestLine className="h-5 w-5" />
PR Comments
{t('gitView.pr.comments.title')}
{pr ? (
<span className="typography-meta text-muted-foreground">PR #{pr.number}</span>
<span className="typography-meta text-muted-foreground">{t('gitView.pr.numberLabel', { number: pr.number })}</span>
) : null}
</DialogTitle>
</DialogHeader>
@@ -1889,7 +1891,7 @@ export const PullRequestSection: React.FC<{
{isLoadingCommentsDetails ? (
<div className="text-center text-muted-foreground py-8 flex items-center justify-center gap-2">
<RiLoader4Line className="h-4 w-4 animate-spin" />
Loading...
{t('gitView.loading.loading')}
</div>
) : null}
@@ -1927,13 +1929,13 @@ export const PullRequestSection: React.FC<{
onClick={() => {
void sendSingleCommentToChat(comment);
}}
aria-label="Send this comment to agent"
aria-label={t('gitView.pr.actions.sendCommentToAgentAria')}
>
<RiAiGenerate2 className="size-3.5" />
Send to agent
{t('gitView.pr.actions.sendToAgent')}
</Button>
</TooltipTrigger>
<TooltipContent><p>Send this comment to agent</p></TooltipContent>
<TooltipContent><p>{t('gitView.pr.actions.sendCommentToAgent')}</p></TooltipContent>
</Tooltip>
</div>
<div className="typography-micro text-muted-foreground">
@@ -1955,7 +1957,7 @@ export const PullRequestSection: React.FC<{
</div>
</div>
) : (
<div className="text-center text-muted-foreground py-8">No comments found.</div>
<div className="text-center text-muted-foreground py-8">{t('gitView.pr.comments.empty')}</div>
)}
</div>
) : null}