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
@@ -42,6 +42,7 @@ import { createProjectPlanFile } from '@/lib/openchamberConfig';
import { resolveProjectForSessionDirectory } from '@/lib/projectResolution';
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
import { useSessions } from '@/sync/sync-context';
import { useI18n } from '@/lib/i18n';
type SubtaskPartLike = Part & {
type: 'subtask';
@@ -86,6 +87,7 @@ const normalizeSubtaskModel = (model: SubtaskPartLike['model']): string | null =
const UserSubtaskPart: React.FC<{ part: SubtaskPartLike }> = ({ part }) => {
const [expanded, setExpanded] = React.useState(false);
const setCurrentSession = useSessionUIStore((state) => state.setCurrentSession);
const { t } = useI18n();
const description = typeof part.description === 'string' ? part.description.trim() : '';
const command = typeof part.command === 'string' ? part.command.trim() : '';
@@ -97,7 +99,7 @@ const UserSubtaskPart: React.FC<{ part: SubtaskPartLike }> = ({ part }) => {
return (
<div className="mt-2">
<div className="flex items-center gap-2 flex-wrap">
<span className="typography-meta font-semibold text-foreground">Delegated task</span>
<span className="typography-meta font-semibold text-foreground">{t('chat.messageBody.subtask.title')}</span>
{command ? (
<span className="inline-flex h-5 items-center rounded px-1.5 text-[11px] leading-none bg-foreground/5 text-muted-foreground">
/{command}
@@ -128,7 +130,7 @@ const UserSubtaskPart: React.FC<{ part: SubtaskPartLike }> = ({ part }) => {
className="typography-meta text-muted-foreground hover:text-foreground transition-colors underline underline-offset-2"
onClick={() => setExpanded((value) => !value)}
>
{expanded ? 'Hide prompt' : 'Show prompt'}
{expanded ? t('chat.messageBody.subtask.hidePrompt') : t('chat.messageBody.subtask.showPrompt')}
</button>
{expanded ? (
<pre className="typography-meta mt-1.5 overflow-x-auto whitespace-pre-wrap break-words text-foreground/85">
@@ -147,7 +149,7 @@ const UserSubtaskPart: React.FC<{ part: SubtaskPartLike }> = ({ part }) => {
void setCurrentSession(taskSessionID);
}}
>
Open subtask session
{t('chat.messageBody.subtask.openSession')}
</button>
</div>
) : null}
@@ -159,6 +161,7 @@ const UserShellActionPart: React.FC<{ part: ShellActionPartLike }> = ({ part })
const [expanded, setExpanded] = React.useState(false);
const [copiedOutput, setCopiedOutput] = React.useState(false);
const copiedResetTimeoutRef = React.useRef<number | null>(null);
const { t } = useI18n();
const command = typeof part.shellAction?.command === 'string' ? part.shellAction.command.trim() : '';
const output = typeof part.shellAction?.output === 'string' ? part.shellAction.output : '';
@@ -197,7 +200,7 @@ const UserShellActionPart: React.FC<{ part: ShellActionPartLike }> = ({ part })
return (
<div className="mt-2">
<div className="flex items-center gap-2 flex-wrap">
<span className="typography-meta font-semibold text-foreground">Shell command</span>
<span className="typography-meta font-semibold text-foreground">{t('chat.messageBody.shellCommand.title')}</span>
{status ? (
<span className={cn(
'inline-flex h-5 items-center rounded px-1.5 text-[11px] leading-none',
@@ -224,7 +227,7 @@ const UserShellActionPart: React.FC<{ part: ShellActionPartLike }> = ({ part })
className="typography-meta text-muted-foreground hover:text-foreground transition-colors underline underline-offset-2"
onClick={() => setExpanded((value) => !value)}
>
{expanded ? 'Hide output' : 'Show output'}
{expanded ? t('chat.messageBody.shellCommand.hideOutput') : t('chat.messageBody.shellCommand.showOutput')}
</button>
<button
type="button"
@@ -232,8 +235,8 @@ const UserShellActionPart: React.FC<{ part: ShellActionPartLike }> = ({ part })
onClick={() => {
void copyOutputToClipboard();
}}
aria-label={copiedOutput ? 'Copied' : 'Copy output'}
title={copiedOutput ? 'Copied' : 'Copy output'}
aria-label={copiedOutput ? t('chat.messageBody.shellCommand.copied') : t('chat.messageBody.shellCommand.copyOutput')}
title={copiedOutput ? t('chat.messageBody.shellCommand.copied') : t('chat.messageBody.shellCommand.copyOutput')}
>
{copiedOutput ? <RiCheckLine className="h-3.5 w-3.5" /> : <RiFileCopyLine className="h-3.5 w-3.5" />}
</button>
@@ -332,6 +335,7 @@ const UserMessageBody: React.FC<{
userActionsMode?: 'inline' | 'external-content' | 'external-actions';
stickyUserHeaderEnabled?: boolean;
}> = ({ messageId, parts, isMobile, hasTouchInput, hasTextContent, onCopyMessage, copiedMessage, onShowPopup, agentMention, onRevert, onFork, userActionsMode = 'inline', stickyUserHeaderEnabled = true }) => {
const { t } = useI18n();
const [copyHintVisible, setCopyHintVisible] = React.useState(false);
const copyHintTimeoutRef = React.useRef<number | null>(null);
@@ -441,7 +445,7 @@ const UserMessageBody: React.FC<{
variant="ghost"
size="icon"
className="h-6 w-6 text-muted-foreground bg-transparent hover:text-foreground hover:!bg-transparent active:!bg-transparent focus-visible:!bg-transparent focus-visible:ring-2 focus-visible:ring-primary/50"
aria-label="Revert to this message"
aria-label={t('chat.messageBody.actions.revertAria')}
onPointerDown={(event) => event.stopPropagation()}
onClick={(event) => {
event.stopPropagation();
@@ -451,7 +455,7 @@ const UserMessageBody: React.FC<{
<RiArrowGoBackLine className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent sideOffset={6}>Revert from here</TooltipContent>
<TooltipContent sideOffset={6}>{t('chat.messageBody.actions.revert')}</TooltipContent>
</Tooltip>
)}
{onFork && (
@@ -462,7 +466,7 @@ const UserMessageBody: React.FC<{
variant="ghost"
size="icon"
className="h-6 w-6 text-muted-foreground bg-transparent hover:text-foreground hover:!bg-transparent active:!bg-transparent focus-visible:!bg-transparent focus-visible:ring-2 focus-visible:ring-primary/50"
aria-label="Fork from this message"
aria-label={t('chat.messageBody.actions.forkAria')}
onPointerDown={(event) => event.stopPropagation()}
onClick={(event) => {
event.stopPropagation();
@@ -472,7 +476,7 @@ const UserMessageBody: React.FC<{
<RiGitBranchLine className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent sideOffset={6}>Fork from here</TooltipContent>
<TooltipContent sideOffset={6}>{t('chat.messageBody.actions.fork')}</TooltipContent>
</Tooltip>
)}
{canCopyMessage && hasCopyableText && (
@@ -484,7 +488,7 @@ const UserMessageBody: React.FC<{
size="icon"
data-visible={copyHintVisible || isMessageCopied ? 'true' : undefined}
className="h-6 w-6 text-muted-foreground bg-transparent hover:text-foreground hover:!bg-transparent active:!bg-transparent focus-visible:!bg-transparent focus-visible:ring-2 focus-visible:ring-primary/50"
aria-label="Copy message text"
aria-label={t('chat.messageBody.actions.copyMessageAria')}
onPointerDown={(event) => event.stopPropagation()}
onClick={handleCopyButtonClick}
onFocus={() => setCopyHintVisible(true)}
@@ -501,7 +505,7 @@ const UserMessageBody: React.FC<{
)}
</Button>
</TooltipTrigger>
<TooltipContent sideOffset={6}>Copy message</TooltipContent>
<TooltipContent sideOffset={6}>{t('chat.messageBody.actions.copyMessage')}</TooltipContent>
</Tooltip>
)}
</div>
@@ -596,6 +600,7 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
turnGroupingContext,
errorMessage,
}) => {
const { t } = useI18n();
const streamPhase = _streamPhase;
void _allowAnimation;
const [copyHintVisible, setCopyHintVisible] = React.useState(false);
@@ -729,11 +734,11 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
const readAloudTooltip = React.useMemo(() => {
if (isTTSPlaying) {
return 'Stop speaking';
return t('chat.messageBody.tts.stopSpeaking');
}
const providerLabel = voiceProvider === 'browser' ? 'Browser' : voiceProvider === 'openai' ? 'OpenAI' : voiceProvider === 'openai-compatible' ? 'Custom' : 'Say';
return `Read aloud (${providerLabel} voice)`;
}, [isTTSPlaying, voiceProvider]);
return t('chat.messageBody.tts.readAloudWithProvider', { provider: providerLabel });
}, [isTTSPlaying, t, voiceProvider]);
const currentSession = React.useMemo(() => {
if (!currentSessionId) {
@@ -979,7 +984,7 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
return;
}
if (!currentProjectRef) {
toast.error('No project found for this session');
toast.error(t('chat.messageBody.toast.noProject'));
return;
}
@@ -990,14 +995,14 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
body: assistantPlanText,
});
if (!created) {
toast.error('Failed to save plan');
toast.error(t('chat.messageBody.toast.savePlanFailed'));
return;
}
window.dispatchEvent(new CustomEvent('openchamber:project-plan-saved', {
detail: { projectId: currentProjectRef.id },
}));
setIsPlanDialogOpen(false);
toast.success('Plan saved');
toast.success(t('chat.messageBody.toast.planSaved'));
} finally {
setIsSavingPlan(false);
}
@@ -1104,10 +1109,10 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
document.body.removeChild(link);
}
toast.success('Image saved');
toast.success(t('chat.messageBody.toast.imageSaved'));
} catch (error) {
console.error('Failed to generate image:', error);
toast.error('Failed to generate image');
toast.error(t('chat.messageBody.toast.generateImageFailed'));
} finally {
if (wrapper && wrapper.parentNode) {
wrapper.parentNode.removeChild(wrapper);
@@ -1424,7 +1429,7 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
!hasCopyableText && 'opacity-50'
)}
disabled={!hasCopyableText}
aria-label="Copy message text"
aria-label={t('chat.messageBody.actions.copyMessageAria')}
aria-hidden={!hasCopyableText}
onPointerDown={(event) => event.stopPropagation()}
onClick={handleCopyButtonClick}
@@ -1446,7 +1451,7 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
)}
</Button>
</TooltipTrigger>
<TooltipContent sideOffset={6}>Copy answer</TooltipContent>
<TooltipContent sideOffset={6}>{t('chat.messageBody.actions.copyAnswer')}</TooltipContent>
</Tooltip>
)}
<Tooltip delayDuration={1000}>
@@ -1470,7 +1475,7 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
)}
</Button>
</TooltipTrigger>
<TooltipContent sideOffset={6}>{isSharing ? 'Saving image...' : 'Save as image'}</TooltipContent>
<TooltipContent sideOffset={6}>{isSharing ? t('chat.messageBody.actions.savingImage') : t('chat.messageBody.actions.saveAsImage')}</TooltipContent>
</Tooltip>
{!isVSCodeRuntime() ? (
<Tooltip delayDuration={1000}>
@@ -1490,7 +1495,7 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
<RiBookletLine className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent sideOffset={6}>Save as plan</TooltipContent>
<TooltipContent sideOffset={6}>{t('chat.messageBody.actions.saveAsPlan')}</TooltipContent>
</Tooltip>
) : null}
<Tooltip delayDuration={1000}>
@@ -1506,7 +1511,7 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
<RiChatNewLine className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent sideOffset={6}>Start new session from this answer</TooltipContent>
<TooltipContent sideOffset={6}>{t('chat.messageBody.actions.startNewSession')}</TooltipContent>
</Tooltip>
<Tooltip delayDuration={1000}>
<TooltipTrigger asChild>
@@ -1521,7 +1526,7 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
<ArrowsMerge className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent sideOffset={6}>Start new multi-run from this answer</TooltipContent>
<TooltipContent sideOffset={6}>{t('chat.messageBody.actions.startNewMultiRun')}</TooltipContent>
</Tooltip>
{showMessageTTSButtons && hasCopyableText && (
@@ -1535,7 +1540,7 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
'h-8 w-8 bg-transparent hover:!bg-transparent active:!bg-transparent focus-visible:!bg-transparent focus-visible:ring-2 focus-visible:ring-primary/50',
isTTSPlaying ? 'text-green-500' : 'text-muted-foreground hover:text-foreground'
)}
aria-label={isTTSPlaying ? 'Stop speaking' : 'Read aloud'}
aria-label={isTTSPlaying ? t('chat.messageBody.tts.stopSpeaking') : t('chat.messageBody.tts.readAloud')}
onPointerDown={(event) => event.stopPropagation()}
onClick={handleTTSClick}
>