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
@@ -9,6 +9,7 @@ import {
|
||||
} from '@/components/ui/select';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
export interface AgentSelectorProps {
|
||||
/** Currently selected agent name (empty string for no agent) */
|
||||
@@ -34,6 +35,7 @@ export const AgentSelector: React.FC<AgentSelectorProps> = ({
|
||||
disabled,
|
||||
id,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const getVisibleAgents = useConfigStore((state) => state.getVisibleAgents);
|
||||
const loadAgents = useConfigStore((state) => state.loadAgents);
|
||||
const defaultAgentName = useConfigStore((state) => state.currentAgentName);
|
||||
@@ -91,7 +93,7 @@ export const AgentSelector: React.FC<AgentSelectorProps> = ({
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<SelectValue placeholder="Select an agent" />
|
||||
<SelectValue placeholder={t('multirun.agentSelector.placeholder')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent fitContent>
|
||||
{selectableAgents.length > 0 && (
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { useGitStore, useGitBranches, useGitLoadingBranches } from '@/stores/useGitStore';
|
||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||
import { getRootBranch } from '@/lib/worktrees/worktreeStatus';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
/** localStorage key matching NewWorktreeDialog */
|
||||
const LAST_SOURCE_BRANCH_KEY = 'oc:lastWorktreeSourceBranch';
|
||||
@@ -101,6 +102,7 @@ export const BranchSelector: React.FC<BranchSelectorProps> = ({
|
||||
disabled,
|
||||
id,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const { localBranches, remoteBranches, isLoading, isGitRepository } = useBranchOptions(directory);
|
||||
const allBranches = React.useMemo(
|
||||
() => [...localBranches, ...remoteBranches.map(b => `remotes/${b}`)],
|
||||
@@ -151,22 +153,22 @@ export const BranchSelector: React.FC<BranchSelectorProps> = ({
|
||||
size="lg"
|
||||
className={className ?? 'w-fit typography-meta text-foreground'}
|
||||
>
|
||||
<SelectValue placeholder={isLoading ? 'Loading branches…' : 'Select source branch...'} />
|
||||
<SelectValue placeholder={isLoading ? t('multiRun.branchSelector.status.loadingBranches') : t('multiRun.branchSelector.placeholder.selectSourceBranch')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-[280px] max-w-[320px]">
|
||||
{isLoading ? (
|
||||
<div className="px-2 py-4 text-center typography-meta text-muted-foreground">
|
||||
Loading branches...
|
||||
{t('multiRun.branchSelector.status.loadingBranches')}
|
||||
</div>
|
||||
) : localBranches.length === 0 && remoteBranches.length === 0 ? (
|
||||
<div className="px-2 py-4 text-center typography-meta text-muted-foreground">
|
||||
No branches found
|
||||
{t('multiRun.branchSelector.status.noBranchesFound')}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{localBranches.length > 0 && (
|
||||
<SelectGroup>
|
||||
<SelectLabel className="font-semibold text-foreground">Local branches</SelectLabel>
|
||||
<SelectLabel className="font-semibold text-foreground">{t('multiRun.branchSelector.groups.localBranches')}</SelectLabel>
|
||||
{localBranches.map((branch) => (
|
||||
<SelectItem key={branch} value={branch} className="whitespace-normal break-all">
|
||||
{branch}
|
||||
@@ -179,7 +181,7 @@ export const BranchSelector: React.FC<BranchSelectorProps> = ({
|
||||
)}
|
||||
{remoteBranches.length > 0 && (
|
||||
<SelectGroup>
|
||||
<SelectLabel className="font-semibold text-foreground">Remote branches</SelectLabel>
|
||||
<SelectLabel className="font-semibold text-foreground">{t('multiRun.branchSelector.groups.remoteBranches')}</SelectLabel>
|
||||
{remoteBranches.map((branch) => (
|
||||
<SelectItem key={`remotes/${branch}`} value={`remotes/${branch}`} className="whitespace-normal break-all">
|
||||
{branch}
|
||||
@@ -193,7 +195,7 @@ export const BranchSelector: React.FC<BranchSelectorProps> = ({
|
||||
</Select>
|
||||
|
||||
{isGitRepository === false && (
|
||||
<p className="typography-micro text-muted-foreground/70 mt-2">Not in a git repository.</p>
|
||||
<p className="typography-micro text-muted-foreground/70 mt-2">{t('multiRun.branchSelector.status.notInGitRepository')}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ import { isIMECompositionEvent } from '@/lib/ime';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { useModelLists } from '@/hooks/useModelLists';
|
||||
import type { ModelMetadata } from '@/types';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
/** Chip height class - shared between chips and add button */
|
||||
const CHIP_HEIGHT_CLASS = 'h-7';
|
||||
@@ -110,11 +111,12 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
|
||||
onRemove,
|
||||
onUpdate,
|
||||
minModels,
|
||||
addButtonLabel = 'Add model',
|
||||
addButtonLabel,
|
||||
showChips = true,
|
||||
maxModels,
|
||||
addButtonClassName,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const providers = useConfigStore((state) => state.providers);
|
||||
const modelsMetadata = useConfigStore((state) => state.modelsMetadata);
|
||||
const { favoriteModelsList, recentModelsList } = useModelLists();
|
||||
@@ -334,7 +336,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<RiAddLine className="h-3.5 w-3.5 mr-1" />
|
||||
{addButtonLabel}
|
||||
{addButtonLabel ?? t('multirun.modelMultiSelect.actions.addModel')}
|
||||
</Button>
|
||||
|
||||
{isOpen && (() => {
|
||||
@@ -414,7 +416,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
|
||||
<Input
|
||||
ref={searchInputRef}
|
||||
type="text"
|
||||
placeholder="Search models"
|
||||
placeholder={t('multirun.modelMultiSelect.search.placeholder')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
@@ -431,7 +433,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
|
||||
<div className="p-1">
|
||||
{!hasResults && (
|
||||
<div className="px-2 py-4 text-center typography-meta text-muted-foreground">
|
||||
No models found
|
||||
{t('multirun.modelMultiSelect.search.noResults')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -440,7 +442,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
|
||||
<>
|
||||
<div className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider sticky top-0 z-10 -mx-1 flex items-center gap-2 border-b border-border/30 px-3 py-1.5 [background:linear-gradient(var(--surface-elevated),var(--surface-elevated)),linear-gradient(var(--surface-background),var(--surface-background))]">
|
||||
<RiStarFill className="h-4 w-4 text-primary" />
|
||||
Favorites
|
||||
{t('multirun.modelMultiSelect.sections.favorites')}
|
||||
</div>
|
||||
{filteredFavorites.map(({ model, providerID, modelID }) => {
|
||||
const idx = currentFlatIndex++;
|
||||
@@ -455,7 +457,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
|
||||
{filteredFavorites.length > 0 && <div className="h-px bg-border/40 my-1" />}
|
||||
<div className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider sticky top-0 z-10 -mx-1 flex items-center gap-2 border-b border-border/30 px-3 py-1.5 [background:linear-gradient(var(--surface-elevated),var(--surface-elevated)),linear-gradient(var(--surface-background),var(--surface-background))]">
|
||||
<RiTimeLine className="h-4 w-4" />
|
||||
Recent
|
||||
{t('multirun.modelMultiSelect.sections.recent')}
|
||||
</div>
|
||||
{filteredRecents.map(({ model, providerID, modelID }) => {
|
||||
const idx = currentFlatIndex++;
|
||||
@@ -491,7 +493,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
|
||||
|
||||
{/* Keyboard hints footer */}
|
||||
<div className="px-3 pt-1 pb-1.5 border-t border-border/40 typography-micro text-muted-foreground">
|
||||
↑↓ navigate • Enter select • Esc close
|
||||
{t('multirun.modelMultiSelect.keyboard.hint')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -544,11 +546,11 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
|
||||
variantValue === DEFAULT_VARIANT_VALUE ? 'text-muted-foreground' : 'text-[color:var(--status-info)]'
|
||||
)}
|
||||
/>
|
||||
<SelectValue placeholder="Thinking" />
|
||||
<SelectValue placeholder={t('multirun.modelMultiSelect.variant.placeholder')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent fitContent>
|
||||
<SelectItem value={DEFAULT_VARIANT_VALUE} className="pr-2 [&>span:first-child]:hidden">
|
||||
Default
|
||||
{t('multirun.modelMultiSelect.variant.default')}
|
||||
</SelectItem>
|
||||
{variantKeys.map((variant) => (
|
||||
<SelectItem key={variant} value={variant} className="pr-2 [&>span:first-child]:hidden">
|
||||
@@ -568,7 +570,9 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
|
||||
{/* Validation hint */}
|
||||
{minModels !== undefined && selectedModels.length < minModels && (
|
||||
<p className="typography-micro text-muted-foreground">
|
||||
Select from {minModels} {maxModels !== undefined ? `to ${maxModels} models` : ''}.
|
||||
{maxModels !== undefined
|
||||
? t('multirun.modelMultiSelect.validation.minToMax', { min: minModels, max: maxModels })
|
||||
: t('multirun.modelMultiSelect.validation.minOnly', { min: minModels })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useThemeSystem } from '@/contexts/useThemeSystem';
|
||||
import { PROJECT_ICON_MAP, PROJECT_COLOR_MAP, getProjectIconImageUrl } from '@/lib/projectMeta';
|
||||
import type { ProjectEntry } from '@/lib/api/types';
|
||||
import { startDesktopWindowDrag } from '@/lib/desktopNative';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
/** Max file size in bytes (10MB) */
|
||||
const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
||||
@@ -93,6 +94,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
onCancel,
|
||||
isWindowed = false,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const [name, setName] = React.useState('');
|
||||
const [prompt, setPrompt] = React.useState(() => initialPrompt ?? '');
|
||||
const [selectedModels, setSelectedModels] = React.useState<ModelSelectionWithId[]>([]);
|
||||
@@ -358,7 +360,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
toast.error(`File "${file.name}" is too large (max 10MB)`);
|
||||
toast.error(t('multirun.launcher.toast.fileTooLarge', { fileName: file.name }));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -382,12 +384,16 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
attachedCount++;
|
||||
} catch (error) {
|
||||
console.error('File attach failed', error);
|
||||
toast.error(`Failed to attach "${file.name}"`);
|
||||
toast.error(t('multirun.launcher.toast.attachFailed', { fileName: file.name }));
|
||||
}
|
||||
}
|
||||
|
||||
if (attachedCount > 0) {
|
||||
toast.success(`Attached ${attachedCount} file${attachedCount > 1 ? 's' : ''}`);
|
||||
toast.success(
|
||||
attachedCount === 1
|
||||
? t('multirun.launcher.toast.attachedSingle', { count: attachedCount })
|
||||
: t('multirun.launcher.toast.attachedPlural', { count: attachedCount })
|
||||
);
|
||||
}
|
||||
|
||||
if (fileInputRef.current) {
|
||||
@@ -596,7 +602,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
)}
|
||||
style={{ borderColor: 'var(--interactive-border)' }}
|
||||
>
|
||||
<h1 className="typography-ui-label font-medium">New Multi-Run</h1>
|
||||
<h1 className="typography-ui-label font-medium">{t('multirun.launcher.title')}</h1>
|
||||
{onCancel && (
|
||||
<div className="absolute right-0 flex items-center pr-3">
|
||||
<Tooltip delayDuration={500}>
|
||||
@@ -604,14 +610,14 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
aria-label="Close (Esc)"
|
||||
aria-label={t('multirun.launcher.actions.closeEsc')}
|
||||
className="inline-flex h-9 w-9 items-center justify-center p-2 rounded-lg text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary app-region-no-drag"
|
||||
>
|
||||
<RiCloseLine className="h-5 w-5" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Close (Esc)</p>
|
||||
<p>{t('multirun.launcher.actions.closeEsc')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -628,7 +634,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-3">
|
||||
{/* Project */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<FieldLabel htmlFor="multirun-project" required>Project</FieldLabel>
|
||||
<FieldLabel htmlFor="multirun-project" required>{t('multirun.launcher.project.label')}</FieldLabel>
|
||||
{projects.length > 0 ? (
|
||||
<Select
|
||||
value={selectedProjectId ?? undefined}
|
||||
@@ -638,7 +644,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
{selectedProject ? (
|
||||
<SelectValue>{renderProjectLabel(selectedProject)}</SelectValue>
|
||||
) : (
|
||||
<SelectValue placeholder="Select project" />
|
||||
<SelectValue placeholder={t('multirun.launcher.project.placeholder')} />
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent fitContent>
|
||||
@@ -650,7 +656,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<p className="typography-micro text-muted-foreground py-2">Add a project first.</p>
|
||||
<p className="typography-micro text-muted-foreground py-2">{t('multirun.launcher.project.empty')}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -659,15 +665,15 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
<FieldLabel
|
||||
htmlFor="group-name"
|
||||
required
|
||||
info={<InfoTip>Used for worktree directory and branch names</InfoTip>}
|
||||
info={<InfoTip>{t('multirun.launcher.groupName.info')}</InfoTip>}
|
||||
>
|
||||
Group name
|
||||
{t('multirun.launcher.groupName.label')}
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id="group-name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="feature-auth, bugfix-login"
|
||||
placeholder={t('multirun.launcher.groupName.placeholder')}
|
||||
className="typography-meta w-full"
|
||||
required
|
||||
/>
|
||||
@@ -677,9 +683,9 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
<div className="flex flex-col gap-1">
|
||||
<FieldLabel
|
||||
htmlFor="multirun-worktree-base-branch"
|
||||
info={<InfoTip>New branch created from this base per model</InfoTip>}
|
||||
info={<InfoTip>{t('multirun.launcher.baseBranch.info')}</InfoTip>}
|
||||
>
|
||||
Base branch
|
||||
{t('multirun.launcher.baseBranch.label')}
|
||||
</FieldLabel>
|
||||
<BranchSelector
|
||||
directory={selectedProjectDirectory}
|
||||
@@ -693,9 +699,9 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
<div className="flex flex-col gap-1">
|
||||
<FieldLabel
|
||||
htmlFor="multirun-agent"
|
||||
info={<InfoTip>Agent used for all runs. Defaults to your configured agent.</InfoTip>}
|
||||
info={<InfoTip>{t('multirun.launcher.agent.info')}</InfoTip>}
|
||||
>
|
||||
Agent
|
||||
{t('multirun.launcher.agent.label')}
|
||||
</FieldLabel>
|
||||
<AgentSelector
|
||||
value={selectedAgent}
|
||||
@@ -710,7 +716,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
<CollapsibleTrigger className="w-full flex items-center gap-2 py-1.5 px-2 -mx-2 rounded-lg hover:bg-[var(--interactive-hover)]/50 transition-colors group">
|
||||
<RiTerminalLine className="h-3.5 w-3.5 text-muted-foreground/70" />
|
||||
<span className="typography-meta font-medium text-muted-foreground group-hover:text-foreground transition-colors">
|
||||
Setup commands
|
||||
{t('multirun.launcher.setupCommands.label')}
|
||||
</span>
|
||||
{configuredSetupCount > 0 && (
|
||||
<span
|
||||
@@ -733,7 +739,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
<CollapsibleContent>
|
||||
<div className="pt-2 space-y-1.5">
|
||||
{isLoadingSetupCommands ? (
|
||||
<p className="typography-meta text-muted-foreground/70 px-2">Loading...</p>
|
||||
<p className="typography-meta text-muted-foreground/70 px-2">{t('multirun.launcher.setupCommands.loading')}</p>
|
||||
) : (
|
||||
<>
|
||||
{setupCommands.map((command, index) => (
|
||||
@@ -745,7 +751,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
newCommands[index] = e.target.value;
|
||||
setSetupCommands(newCommands);
|
||||
}}
|
||||
placeholder="bun install"
|
||||
placeholder={t('multirun.launcher.setupCommands.commandPlaceholder')}
|
||||
className="h-8 flex-1 font-mono text-xs"
|
||||
/>
|
||||
<button
|
||||
@@ -755,7 +761,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
setSetupCommands(newCommands);
|
||||
}}
|
||||
className="flex-shrink-0 flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:text-destructive hover:bg-destructive/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
|
||||
aria-label="Remove command"
|
||||
aria-label={t('multirun.launcher.setupCommands.removeCommandAria')}
|
||||
>
|
||||
<RiCloseLine className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
@@ -767,7 +773,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
className="flex items-center gap-1 typography-meta text-muted-foreground hover:text-foreground transition-colors px-1"
|
||||
>
|
||||
<RiAddLine className="h-3 w-3" />
|
||||
Add command
|
||||
{t('multirun.launcher.setupCommands.addCommand')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
@@ -777,7 +783,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
|
||||
{/* ── Prompt ── */}
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<FieldLabel htmlFor="prompt" required>Prompt</FieldLabel>
|
||||
<FieldLabel htmlFor="prompt" required>{t('multirun.launcher.prompt.label')}</FieldLabel>
|
||||
<div className="relative">
|
||||
<Textarea
|
||||
id="prompt"
|
||||
@@ -790,7 +796,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
updateAutocompleteState(nextPrompt, cursorPosition);
|
||||
}}
|
||||
onKeyDown={handlePromptKeyDown}
|
||||
placeholder="Enter the prompt to send to all models..."
|
||||
placeholder={t('multirun.launcher.prompt.placeholder')}
|
||||
className="typography-meta min-h-[100px] max-h-[300px] resize-none overflow-y-auto field-sizing-content"
|
||||
required
|
||||
/>
|
||||
@@ -847,10 +853,10 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
className="inline-flex items-center gap-1 h-6 px-2 rounded-md typography-micro text-muted-foreground hover:text-foreground hover:bg-[var(--interactive-hover)]/50 transition-colors"
|
||||
>
|
||||
<RiAttachment2 className="h-3 w-3" />
|
||||
Attach
|
||||
{t('multirun.launcher.attachments.attach')}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Same files sent to all runs</TooltipContent>
|
||||
<TooltipContent>{t('multirun.launcher.attachments.tooltip')}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{attachedFiles.map((file) => (
|
||||
@@ -886,9 +892,9 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<FieldLabel
|
||||
required
|
||||
info={<InfoTip>Select 2–{MAX_MODELS} models. Same model can be added multiple times.</InfoTip>}
|
||||
info={<InfoTip>{t('multirun.launcher.models.info', { max: MAX_MODELS })}</InfoTip>}
|
||||
>
|
||||
Models
|
||||
{t('multirun.launcher.models.label')}
|
||||
</FieldLabel>
|
||||
<ModelMultiSelect
|
||||
selectedModels={selectedModels}
|
||||
@@ -927,7 +933,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
size="sm"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
{t('multirun.launcher.actions.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -935,9 +941,9 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
disabled={!isValid || isSubmitting}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
'Creating...'
|
||||
t('multirun.launcher.actions.creating')
|
||||
) : (
|
||||
<>Start ({selectedModels.length} models)</>
|
||||
<>{t('multirun.launcher.actions.startWithModelCount', { count: selectedModels.length })}</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user