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
@@ -33,6 +33,7 @@ import { renderMagicPrompt } from '@/lib/magicPrompts';
|
||||
import { createWorktreeSessionForNewBranch } from '@/lib/worktreeSessionCreator';
|
||||
import { generateBranchSlug } from '@/lib/git/branchNameGenerator';
|
||||
import type { GitHubIssue, GitHubIssueComment, GitHubIssuesListResult, GitHubIssueSummary } from '@/lib/api/types';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
const parseIssueNumber = (value: string): number | null => {
|
||||
const trimmed = value.trim();
|
||||
@@ -77,6 +78,7 @@ export function GitHubIssuePickerDialog({
|
||||
mode?: 'createSession' | 'select';
|
||||
onSelect?: (issue: { number: number; title: string; url: string; contextText: string; author?: { login: string; avatarUrl?: string } }) => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const { github } = useRuntimeAPIs();
|
||||
const githubAuthStatus = useGitHubAuthStore((state) => state.status);
|
||||
const githubAuthChecked = useGitHubAuthStore((state) => state.hasChecked);
|
||||
@@ -101,7 +103,7 @@ export function GitHubIssuePickerDialog({
|
||||
const refresh = React.useCallback(async () => {
|
||||
if (!projectDirectory) {
|
||||
setResult(null);
|
||||
setError('No active project');
|
||||
setError(t('session.githubIssuePicker.error.noActiveProject'));
|
||||
return;
|
||||
}
|
||||
if (githubAuthChecked && githubAuthStatus?.connected === false) {
|
||||
@@ -114,7 +116,7 @@ export function GitHubIssuePickerDialog({
|
||||
}
|
||||
if (!github?.issuesList) {
|
||||
setResult(null);
|
||||
setError('GitHub runtime API unavailable');
|
||||
setError(t('session.githubIssuePicker.error.runtimeUnavailable'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -152,11 +154,11 @@ export function GitHubIssuePickerDialog({
|
||||
setHasMore(Boolean(next.hasMore));
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
toast.error('Failed to load more issues', { description: message });
|
||||
toast.error(t('session.githubIssuePicker.toast.loadMoreFailed'), { description: message });
|
||||
} finally {
|
||||
setIsLoadingMore(false);
|
||||
}
|
||||
}, [github, hasMore, isLoading, isLoadingMore, page, projectDirectory]);
|
||||
}, [github, hasMore, isLoading, isLoadingMore, page, projectDirectory, t]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!open) {
|
||||
@@ -270,11 +272,11 @@ export function GitHubIssuePickerDialog({
|
||||
if (mode === 'select') {
|
||||
// In select mode, fetch full issue details and return via onSelect
|
||||
if (!projectDirectory) {
|
||||
toast.error('No active project');
|
||||
toast.error(t('session.githubIssuePicker.error.noActiveProject'));
|
||||
return;
|
||||
}
|
||||
if (!github?.issueGet || !github?.issueComments) {
|
||||
toast.error('GitHub runtime API unavailable');
|
||||
toast.error(t('session.githubIssuePicker.error.runtimeUnavailable'));
|
||||
return;
|
||||
}
|
||||
if (startingIssueNumber) return;
|
||||
@@ -282,24 +284,24 @@ export function GitHubIssuePickerDialog({
|
||||
try {
|
||||
const issueRes = await github.issueGet(projectDirectory, issueNumber);
|
||||
if (issueRes.connected === false) {
|
||||
toast.error('GitHub not connected');
|
||||
toast.error(t('session.githubIssuePicker.error.notConnected'));
|
||||
return;
|
||||
}
|
||||
if (!issueRes.repo) {
|
||||
toast.error('Repo not resolvable', {
|
||||
description: 'origin remote must be a GitHub URL',
|
||||
toast.error(t('session.githubIssuePicker.error.repoNotResolvable'), {
|
||||
description: t('session.githubIssuePicker.error.repoMustBeGithub'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const issue = issueRes.issue;
|
||||
if (!issue) {
|
||||
toast.error('Issue not found');
|
||||
toast.error(t('session.githubIssuePicker.error.issueNotFound'));
|
||||
return;
|
||||
}
|
||||
|
||||
const commentsRes = await github.issueComments(projectDirectory, issueNumber);
|
||||
if (commentsRes.connected === false) {
|
||||
toast.error('GitHub not connected');
|
||||
toast.error(t('session.githubIssuePicker.error.notConnected'));
|
||||
return;
|
||||
}
|
||||
const comments = commentsRes.comments ?? [];
|
||||
@@ -322,7 +324,7 @@ export function GitHubIssuePickerDialog({
|
||||
onOpenChange(false);
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
toast.error('Failed to load issue details', { description: message });
|
||||
toast.error(t('session.githubIssuePicker.toast.loadIssueDetailsFailed'), { description: message });
|
||||
} finally {
|
||||
setStartingIssueNumber(null);
|
||||
}
|
||||
@@ -330,11 +332,11 @@ export function GitHubIssuePickerDialog({
|
||||
}
|
||||
|
||||
if (!projectDirectory) {
|
||||
toast.error('No active project');
|
||||
toast.error(t('session.githubIssuePicker.error.noActiveProject'));
|
||||
return;
|
||||
}
|
||||
if (!github?.issueGet || !github?.issueComments) {
|
||||
toast.error('GitHub runtime API unavailable');
|
||||
toast.error(t('session.githubIssuePicker.error.runtimeUnavailable'));
|
||||
return;
|
||||
}
|
||||
if (startingIssueNumber) return;
|
||||
@@ -342,24 +344,24 @@ export function GitHubIssuePickerDialog({
|
||||
try {
|
||||
const issueRes = await github.issueGet(projectDirectory, issueNumber);
|
||||
if (issueRes.connected === false) {
|
||||
toast.error('GitHub not connected');
|
||||
toast.error(t('session.githubIssuePicker.error.notConnected'));
|
||||
return;
|
||||
}
|
||||
if (!issueRes.repo) {
|
||||
toast.error('Repo not resolvable', {
|
||||
description: 'origin remote must be a GitHub URL',
|
||||
toast.error(t('session.githubIssuePicker.error.repoNotResolvable'), {
|
||||
description: t('session.githubIssuePicker.error.repoMustBeGithub'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const issue = issueRes.issue;
|
||||
if (!issue) {
|
||||
toast.error('Issue not found');
|
||||
toast.error(t('session.githubIssuePicker.error.issueNotFound'));
|
||||
return;
|
||||
}
|
||||
|
||||
const commentsRes = await github.issueComments(projectDirectory, issueNumber);
|
||||
if (commentsRes.connected === false) {
|
||||
toast.error('GitHub not connected');
|
||||
toast.error(t('session.githubIssuePicker.error.notConnected'));
|
||||
return;
|
||||
}
|
||||
const comments = commentsRes.comments ?? [];
|
||||
@@ -406,7 +408,7 @@ export function GitHubIssuePickerDialog({
|
||||
const modelID = defaultModel?.modelID || configState.currentModelId || lastUsedProvider?.modelID;
|
||||
const agentName = resolveDefaultAgentName() || configState.currentAgentName || undefined;
|
||||
if (!providerID || !modelID) {
|
||||
toast.error('No model selected');
|
||||
toast.error(t('session.githubIssuePicker.error.noModelSelected'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -470,31 +472,31 @@ export function GitHubIssuePickerDialog({
|
||||
],
|
||||
}).catch((e) => {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
toast.error('Failed to send issue context', {
|
||||
toast.error(t('session.githubIssuePicker.toast.sendContextFailed'), {
|
||||
description: message,
|
||||
});
|
||||
});
|
||||
|
||||
toast.success('Session created from issue');
|
||||
toast.success(t('session.githubIssuePicker.toast.sessionCreated'));
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
toast.error('Failed to start session', { description: message });
|
||||
toast.error(t('session.githubIssuePicker.toast.startSessionFailed'), { description: message });
|
||||
} finally {
|
||||
setStartingIssueNumber(null);
|
||||
}
|
||||
}, [createInWorktree, github, mode, onOpenChange, onSelect, projectDirectory, resolveDefaultAgentName, resolveDefaultModelSelection, resolveDefaultVariant, startingIssueNumber]);
|
||||
}, [createInWorktree, github, mode, onOpenChange, onSelect, projectDirectory, resolveDefaultAgentName, resolveDefaultModelSelection, resolveDefaultVariant, startingIssueNumber, t]);
|
||||
|
||||
const title = mode === 'select' ? 'Link GitHub Issue' : 'New Session From GitHub Issue';
|
||||
const title = mode === 'select' ? t('session.githubIssuePicker.title.select') : t('session.githubIssuePicker.title.createSession');
|
||||
const description = mode === 'select'
|
||||
? 'Select an issue to link to this session.'
|
||||
: 'Seeds a new session with hidden issue context (title/body/labels/comments).';
|
||||
? t('session.githubIssuePicker.description.select')
|
||||
: t('session.githubIssuePicker.description.createSession');
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<div className="relative mt-2">
|
||||
<RiSearchLine className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search by title or #123, or paste issue URL"
|
||||
placeholder={t('session.githubIssuePicker.searchPlaceholder')}
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className="pl-9 w-full"
|
||||
@@ -503,26 +505,26 @@ export function GitHubIssuePickerDialog({
|
||||
|
||||
<div className={cn(isMobile ? 'min-h-0 mt-2' : 'flex-1 overflow-y-auto mt-2')}>
|
||||
{!projectDirectory ? (
|
||||
<div className="text-center text-muted-foreground py-8">No active project selected.</div>
|
||||
<div className="text-center text-muted-foreground py-8">{t('session.githubIssuePicker.empty.noActiveProject')}</div>
|
||||
) : null}
|
||||
|
||||
{!github ? (
|
||||
<div className="text-center text-muted-foreground py-8">GitHub runtime API unavailable.</div>
|
||||
<div className="text-center text-muted-foreground py-8">{t('session.githubIssuePicker.empty.runtimeUnavailable')}</div>
|
||||
) : null}
|
||||
|
||||
{isLoading ? (
|
||||
<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 issues...
|
||||
{t('session.githubIssuePicker.loading.issues')}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{connected === false ? (
|
||||
<div className="text-center text-muted-foreground py-8 space-y-3">
|
||||
<div>GitHub not connected. Connect your GitHub account in settings.</div>
|
||||
<div>{t('session.githubIssuePicker.empty.notConnected')}</div>
|
||||
<div className="flex justify-center">
|
||||
<Button variant="outline" size="sm" onClick={openGitHubSettings}>
|
||||
Open settings
|
||||
{t('session.githubIssuePicker.actions.openSettings')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -542,7 +544,7 @@ export function GitHubIssuePickerDialog({
|
||||
>
|
||||
<span className="typography-meta text-muted-foreground w-5 text-right flex-shrink-0">#</span>
|
||||
<p className="flex-1 min-w-0 typography-small text-foreground truncate ml-0.5">
|
||||
Use issue #{directNumber}
|
||||
{t('session.githubIssuePicker.actions.useIssue', { number: directNumber })}
|
||||
</p>
|
||||
<div className="flex-shrink-0 h-5 flex items-center mr-2">
|
||||
{startingIssueNumber === directNumber ? (
|
||||
@@ -553,7 +555,7 @@ export function GitHubIssuePickerDialog({
|
||||
) : null}
|
||||
|
||||
{filtered.length === 0 && !isLoading && connected && github && projectDirectory ? (
|
||||
<div className="text-center text-muted-foreground py-8">{query ? 'No issues found' : 'No open issues found'}</div>
|
||||
<div className="text-center text-muted-foreground py-8">{query ? t('session.githubIssuePicker.empty.noIssuesFound') : t('session.githubIssuePicker.empty.noOpenIssuesFound')}</div>
|
||||
) : null}
|
||||
|
||||
{filtered.map((issue) => (
|
||||
@@ -582,7 +584,7 @@ export function GitHubIssuePickerDialog({
|
||||
rel="noopener noreferrer"
|
||||
className="hidden group-hover:flex h-5 w-5 items-center justify-center text-muted-foreground hover:text-foreground transition-colors"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
aria-label="Open in GitHub"
|
||||
aria-label={t('session.githubIssuePicker.actions.openInGitHubAria')}
|
||||
>
|
||||
<RiExternalLinkLine className="h-4 w-4" />
|
||||
</a>
|
||||
@@ -605,10 +607,10 @@ export function GitHubIssuePickerDialog({
|
||||
{isLoadingMore ? (
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<RiLoader4Line className="h-4 w-4 animate-spin" />
|
||||
Loading...
|
||||
{t('session.githubIssuePicker.loading.more')}
|
||||
</span>
|
||||
) : (
|
||||
'Load more'
|
||||
t('session.githubIssuePicker.actions.loadMore')
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
@@ -617,7 +619,7 @@ export function GitHubIssuePickerDialog({
|
||||
|
||||
{mode !== 'select' && (
|
||||
<div className="mt-4 p-3 bg-muted/30 rounded-lg">
|
||||
<p className="typography-meta text-muted-foreground font-medium mb-2">Actions</p>
|
||||
<p className="typography-meta text-muted-foreground font-medium mb-2">{t('session.githubIssuePicker.actions.sectionTitle')}</p>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-2">
|
||||
<div
|
||||
className="flex items-center gap-2 cursor-pointer"
|
||||
@@ -639,7 +641,7 @@ export function GitHubIssuePickerDialog({
|
||||
e.stopPropagation();
|
||||
setCreateInWorktree((v) => !v);
|
||||
}}
|
||||
aria-label="Toggle worktree"
|
||||
aria-label={t('session.githubIssuePicker.actions.toggleWorktreeAria')}
|
||||
className="flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
||||
>
|
||||
{createInWorktree ? (
|
||||
@@ -648,7 +650,7 @@ export function GitHubIssuePickerDialog({
|
||||
<RiCheckboxBlankLine className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
<span className="typography-meta text-muted-foreground">Create in worktree</span>
|
||||
<span className="typography-meta text-muted-foreground">{t('session.githubIssuePicker.actions.createInWorktree')}</span>
|
||||
<span className="typography-meta text-muted-foreground/70 hidden sm:inline">(issue-<number>-<slug>)</span>
|
||||
</div>
|
||||
<div className="hidden sm:block sm:flex-1" />
|
||||
@@ -657,12 +659,12 @@ export function GitHubIssuePickerDialog({
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<a href={repoUrl} target="_blank" rel="noopener noreferrer">
|
||||
<RiExternalLinkLine className="size-4" />
|
||||
Open Repo
|
||||
{t('session.githubIssuePicker.actions.openRepo')}
|
||||
</a>
|
||||
</Button>
|
||||
) : null}
|
||||
<Button variant="outline" size="sm" onClick={refresh} disabled={isLoading || Boolean(startingIssueNumber)}>
|
||||
Refresh
|
||||
{t('session.githubIssuePicker.actions.refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user