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
@@ -12,6 +12,7 @@ import { useSessionMessageRecords } from '@/sync/sync-context';
import { RiLoader4Line, RiSearchLine, RiTimeLine, RiGitBranchLine, RiArrowGoBackLine } from '@remixicon/react';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import type { Part } from '@opencode-ai/sdk/v2';
import { useI18n } from '@/lib/i18n';
interface TimelineDialogProps {
open: boolean;
@@ -21,22 +22,6 @@ interface TimelineDialogProps {
onResumeToLatest?: () => void;
}
// Helper: format relative time (e.g., "2 hours ago")
function formatRelativeTime(timestamp: number): string {
const now = Date.now();
const diffMs = now - timestamp;
const diffSecs = Math.floor(diffMs / 1000);
const diffMins = Math.floor(diffSecs / 60);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffSecs < 60) return 'just now';
if (diffMins < 60) return `${diffMins}m ago`;
if (diffHours < 24) return `${diffHours}h ago`;
if (diffDays < 7) return `${diffDays}d ago`;
return new Date(timestamp).toLocaleDateString();
}
export const TimelineDialog: React.FC<TimelineDialogProps> = ({
open,
onOpenChange,
@@ -44,6 +29,7 @@ export const TimelineDialog: React.FC<TimelineDialogProps> = ({
onScrollByTurnOffset,
onResumeToLatest,
}) => {
const { t } = useI18n();
const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
const messages = useSessionMessageRecords(currentSessionId ?? '');
const revertToMessage = useSessionUIStore((state) => state.revertToMessage);
@@ -52,6 +38,21 @@ export const TimelineDialog: React.FC<TimelineDialogProps> = ({
const [forkingMessageId, setForkingMessageId] = React.useState<string | null>(null);
const [searchQuery, setSearchQuery] = React.useState('');
const formatRelativeTime = React.useCallback((timestamp: number): string => {
const now = Date.now();
const diffMs = now - timestamp;
const diffSecs = Math.floor(diffMs / 1000);
const diffMins = Math.floor(diffSecs / 60);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffSecs < 60) return t('chat.timeline.relative.justNow');
if (diffMins < 60) return t('chat.timeline.relative.minutesAgo', { count: diffMins });
if (diffHours < 24) return t('chat.timeline.relative.hoursAgo', { count: diffHours });
if (diffDays < 7) return t('chat.timeline.relative.daysAgo', { count: diffDays });
return new Date(timestamp).toLocaleDateString();
}, [t]);
// Filter user messages (reversed for newest first)
const userMessages = React.useMemo(() => {
const filtered = messages.filter(m => m.info.role === 'user');
@@ -89,17 +90,17 @@ export const TimelineDialog: React.FC<TimelineDialogProps> = ({
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<RiTimeLine className="h-5 w-5" />
Conversation Timeline
{t('chat.timeline.title')}
</DialogTitle>
<DialogDescription>
Navigate to any point in the conversation or fork a new session
{t('chat.timeline.description')}
</DialogDescription>
</DialogHeader>
<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 messages..."
placeholder={t('chat.timeline.searchPlaceholder')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 w-full"
@@ -109,7 +110,7 @@ export const TimelineDialog: React.FC<TimelineDialogProps> = ({
<div className="flex-1 overflow-y-auto">
{filteredMessages.length === 0 ? (
<div className="text-center text-muted-foreground py-8">
{searchQuery ? 'No messages found' : 'No messages in this session yet'}
{searchQuery ? t('chat.timeline.empty.search') : t('chat.timeline.empty.session')}
</div>
) : (
filteredMessages.map((message) => {
@@ -134,7 +135,7 @@ export const TimelineDialog: React.FC<TimelineDialogProps> = ({
{messageNumber}.
</span>
<p className="flex-1 min-w-0 typography-small text-foreground truncate ml-0.5">
{preview || '[No text content]'}
{preview || t('chat.timeline.noTextContent')}
{preview && preview.length >= 80 && '…'}
</p>
@@ -158,7 +159,7 @@ export const TimelineDialog: React.FC<TimelineDialogProps> = ({
<RiArrowGoBackLine className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent sideOffset={6}>Revert from here</TooltipContent>
<TooltipContent sideOffset={6}>{t('chat.timeline.actions.revertFromHere')}</TooltipContent>
</Tooltip>
<Tooltip delayDuration={1000}>
@@ -179,7 +180,7 @@ export const TimelineDialog: React.FC<TimelineDialogProps> = ({
)}
</button>
</TooltipTrigger>
<TooltipContent sideOffset={6}>Fork from here</TooltipContent>
<TooltipContent sideOffset={6}>{t('chat.timeline.actions.forkFromHere')}</TooltipContent>
</Tooltip>
</div>
</div>
@@ -190,7 +191,7 @@ export const TimelineDialog: React.FC<TimelineDialogProps> = ({
</div>
<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('chat.timeline.actions.title')}</p>
<div className="mb-2 flex items-center gap-2">
<button
type="button"
@@ -200,7 +201,7 @@ export const TimelineDialog: React.FC<TimelineDialogProps> = ({
onOpenChange(false);
}}
>
Previous turn
{t('chat.timeline.actions.previousTurn')}
</button>
<span className="text-muted-foreground/50">/</span>
<button
@@ -211,20 +212,20 @@ export const TimelineDialog: React.FC<TimelineDialogProps> = ({
onOpenChange(false);
}}
>
Latest
{t('chat.timeline.actions.latest')}
</button>
</div>
<div className="flex flex-col gap-1.5 typography-meta text-muted-foreground">
<div className="flex items-center gap-2">
<span>Click on a message to scroll to it in the conversation</span>
<span>{t('chat.timeline.help.clickMessage')}</span>
</div>
<div className="flex items-center gap-2">
<RiArrowGoBackLine className="h-4 w-4 flex-shrink-0" />
<span>Undo to this point (message text will populate input)</span>
<span>{t('chat.timeline.help.undoToPoint')}</span>
</div>
<div className="flex items-center gap-2">
<RiGitBranchLine className="h-4 w-4 flex-shrink-0" />
<span>Create a new session starting from here</span>
<span>{t('chat.timeline.help.createSessionFromHere')}</span>
</div>
</div>
</div>