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
@@ -3,6 +3,7 @@ import type { Session } from '@opencode-ai/sdk/v2';
import { RiLayoutLeftLine } from '@remixicon/react';
import { toast } from '@/components/ui';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useI18n } from '@/lib/i18n';
import { isDesktopLocalOriginActive, isDesktopShell, isTauriShell } from '@/lib/desktop';
import { isDesktopWindowFullscreen as getDesktopWindowFullscreen, onDesktopWindowResized, startDesktopWindowDrag } from '@/lib/desktopNative';
import { sessionEvents } from '@/lib/sessionEvents';
@@ -125,6 +126,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
hideDirectoryControls = false,
showOnlyMainWorkspace = false,
}) => {
const { t } = useI18n();
const [isSessionSearchOpen, setIsSessionSearchOpen] = React.useState(false);
const [sessionSearchQuery, setSessionSearchQuery] = React.useState('');
const sessionSearchContainerRef = React.useRef<HTMLDivElement | null>(null);
@@ -542,8 +544,8 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
const emptyState = (
<div className="py-6 text-center text-muted-foreground">
<p className="typography-ui-label font-semibold">No sessions yet</p>
<p className="typography-meta mt-1">Create your first session to start coding.</p>
<p className="typography-ui-label font-semibold">{t('sessions.sidebar.empty.noSessions.title')}</p>
<p className="typography-meta mt-1">{t('sessions.sidebar.empty.noSessions.description')}</p>
</div>
);
@@ -574,16 +576,16 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
void updateStore.checkForUpdates().then(() => {
const { available, error } = useUpdateStore.getState();
if (error) {
toast.error('Failed to check for updates', { description: error });
toast.error(t('sessions.sidebar.updateCheck.errorTitle'), { description: error });
return;
}
if (!available) {
toast.success('You are on the latest version');
toast.success(t('sessions.sidebar.updateCheck.latestVersion'));
return;
}
setUpdateDialogOpen(true);
});
}, [updateStore]);
}, [t, updateStore]);
const handleOpenSettings = React.useCallback(() => {
if (mobileVariant) {
@@ -664,21 +666,21 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
if (result.success && result.path) {
const added = addProject(result.path, { id: result.projectId });
if (!added) {
toast.error('Failed to add project', {
description: 'Please select a valid directory.',
toast.error(t('sessions.sidebar.directory.errorAddProjectTitle'), {
description: t('sessions.sidebar.directory.errorAddProjectDescription'),
});
}
} else if (result.error && result.error !== 'Directory selection cancelled') {
toast.error('Failed to select directory', {
toast.error(t('sessions.sidebar.directory.errorSelectDirectoryTitle'), {
description: result.error,
});
}
})
.catch((error) => {
console.error('Desktop: Error selecting directory:', error);
toast.error('Failed to select directory');
toast.error(t('sessions.sidebar.directory.errorSelectDirectoryTitle'));
});
}, [addProject, tauriIpcAvailable]);
}, [addProject, t, tauriIpcAvailable]);
// Auto-expand parent session when navigating to a subagent (child) session
React.useEffect(() => {
@@ -722,12 +724,12 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
toggleFolderCollapse(parentId);
}
const newFolder = createFolder(scopeKey, 'New folder', parentId);
const newFolder = createFolder(scopeKey, t('sessions.sidebar.folder.newFolderName'), parentId);
setRenamingFolderId(newFolder.id);
setRenameFolderDraft(newFolder.name);
return newFolder;
},
[collapsedFolderIds, toggleFolderCollapse, createFolder],
[collapsedFolderIds, toggleFolderCollapse, createFolder, t],
);
const toggleGroupSessionLimit = React.useCallback((groupId: string) => {
@@ -890,8 +892,8 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
const searchEmptyState = (
<div className="py-6 text-center text-muted-foreground">
<p className="typography-ui-label font-semibold">No matching sessions</p>
<p className="typography-meta mt-1">Try a different title, branch, folder, or path.</p>
<p className="typography-ui-label font-semibold">{t('sessions.sidebar.empty.noMatches.title')}</p>
<p className="typography-meta mt-1">{t('sessions.sidebar.empty.noMatches.description')}</p>
</div>
);
@@ -1037,9 +1039,9 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
};
return [
{ key: 'active-now' as const, title: 'recent', items: activeNowSessions.map(toItem) },
{ key: 'active-now' as const, title: t('sessions.sidebar.activity.recentTitle'), items: activeNowSessions.map(toItem) },
];
}, [activeNowSessions, sessionSidebarMetaById]);
}, [activeNowSessions, sessionSidebarMetaById, t]);
const recentSessionIds = React.useMemo(() => {
return new Set(activitySections.flatMap((section) => section.items.map((item) => item.node.session.id)));
@@ -1468,15 +1470,31 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
if (ids.length === 0) return;
if (bulkScopeIsArchived) {
const { deletedIds, failedIds } = await deleteSessions(ids);
if (deletedIds.length > 0) toast.success(`Deleted ${deletedIds.length} session${deletedIds.length === 1 ? '' : 's'}`);
if (failedIds.length > 0) toast.error(`Failed to delete ${failedIds.length} session${failedIds.length === 1 ? '' : 's'}`);
if (deletedIds.length > 0) {
toast.success(deletedIds.length === 1
? t('sessions.sidebar.bulkActions.deletedSingle', { count: deletedIds.length })
: t('sessions.sidebar.bulkActions.deletedPlural', { count: deletedIds.length }));
}
if (failedIds.length > 0) {
toast.error(failedIds.length === 1
? t('sessions.sidebar.bulkActions.failedDeleteSingle', { count: failedIds.length })
: t('sessions.sidebar.bulkActions.failedDeletePlural', { count: failedIds.length }));
}
} else {
const { archivedIds, failedIds } = await archiveSessions(ids);
if (archivedIds.length > 0) toast.success(`Archived ${archivedIds.length} session${archivedIds.length === 1 ? '' : 's'}`);
if (failedIds.length > 0) toast.error(`Failed to archive ${failedIds.length} session${failedIds.length === 1 ? '' : 's'}`);
if (archivedIds.length > 0) {
toast.success(archivedIds.length === 1
? t('sessions.sidebar.bulkActions.archivedSingle', { count: archivedIds.length })
: t('sessions.sidebar.bulkActions.archivedPlural', { count: archivedIds.length }));
}
if (failedIds.length > 0) {
toast.error(failedIds.length === 1
? t('sessions.sidebar.bulkActions.failedArchiveSingle', { count: failedIds.length })
: t('sessions.sidebar.bulkActions.failedArchivePlural', { count: failedIds.length }));
}
}
useSessionMultiSelectStore.getState().clear();
}, [archiveSessions, bulkScopeIsArchived, deleteSessions, selectedIds]);
}, [archiveSessions, bulkScopeIsArchived, deleteSessions, selectedIds, t]);
const handleBulkDelete = React.useCallback(() => {
const count = selectedIds.size;
@@ -1576,13 +1594,13 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
type="button"
onClick={toggleSidebar}
className={desktopSidebarToggleButtonClass}
aria-label="Close sessions"
aria-label={t('sessions.sidebar.header.actions.closeSessions')}
>
<RiLayoutLeftLine className="h-[18px] w-[18px]" />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Close sessions</p>
<p>{t('sessions.sidebar.header.actions.closeSessions')}</p>
</TooltipContent>
</Tooltip>
</div>