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
@@ -45,10 +45,11 @@ import { useGitStatus } from '@/stores/useGitStore';
import { useDirectoryShowHidden } from '@/lib/directoryShowHidden';
import { useFilesViewShowGitignored } from '@/lib/filesViewShowGitignored';
import { copyTextToClipboard } from '@/lib/clipboard';
import { cn, getRevealLabel } from '@/lib/utils';
import { cn, getRevealLabelKey } from '@/lib/utils';
import { opencodeClient } from '@/lib/opencode/client';
import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
import { getContextFileOpenFailureMessage, validateContextFileOpen } from '@/lib/contextFileOpenGuard';
import { useI18n } from '@/lib/i18n';
type FileNode = {
name: string;
@@ -171,6 +172,7 @@ const FileRow: React.FC<FileRowProps> = ({
onRevealPath,
onOpenDialog,
}) => {
const { t } = useI18n();
const isDir = node.type === 'directory';
const { canRename, canCreateFile, canCreateFolder, canDelete, canReveal } = permissions;
@@ -256,32 +258,32 @@ const FileRow: React.FC<FileRowProps> = ({
<DropdownMenuContent align="end" side="bottom" onCloseAutoFocus={() => setContextMenuPath(null)}>
{canRename && (
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onOpenDialog('rename', node); }}>
<RiEditLine className="mr-2 h-4 w-4" /> Rename
<RiEditLine className="mr-2 h-4 w-4" /> {t('sidebarFilesTree.menu.rename')}
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={(e) => {
e.stopPropagation();
void copyTextToClipboard(node.path).then((result) => {
if (result.ok) {
toast.success('Path copied');
toast.success(t('sidebarFilesTree.toast.pathCopied'));
return;
}
toast.error('Copy failed');
toast.error(t('sidebarFilesTree.toast.copyFailed'));
});
}}>
<RiFileCopyLine className="mr-2 h-4 w-4" /> Copy Path
<RiFileCopyLine className="mr-2 h-4 w-4" /> {t('sidebarFilesTree.menu.copyPath')}
</DropdownMenuItem>
{!isDir && downloadFile && (
<DropdownMenuItem onClick={(e) => {
e.stopPropagation();
void downloadFile(node.path);
}}>
<RiDownloadLine className="mr-2 h-4 w-4" /> Save
<RiDownloadLine className="mr-2 h-4 w-4" /> {t('sidebarFilesTree.menu.save')}
</DropdownMenuItem>
)}
{canReveal && (
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onRevealPath(node.path); }}>
<RiFolderReceivedLine className="mr-2 h-4 w-4" /> {getRevealLabel()}
<RiFolderReceivedLine className="mr-2 h-4 w-4" /> {t(getRevealLabelKey())}
</DropdownMenuItem>
)}
{isDir && (canCreateFile || canCreateFolder) && (
@@ -289,12 +291,12 @@ const FileRow: React.FC<FileRowProps> = ({
<DropdownMenuSeparator />
{canCreateFile && (
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onOpenDialog('createFile', node); }}>
<RiFileAddLine className="mr-2 h-4 w-4" /> New File
<RiFileAddLine className="mr-2 h-4 w-4" /> {t('sidebarFilesTree.menu.newFile')}
</DropdownMenuItem>
)}
{canCreateFolder && (
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onOpenDialog('createFolder', node); }}>
<RiFolderAddLine className="mr-2 h-4 w-4" /> New Folder
<RiFolderAddLine className="mr-2 h-4 w-4" /> {t('sidebarFilesTree.menu.newFolder')}
</DropdownMenuItem>
)}
</>
@@ -306,7 +308,7 @@ const FileRow: React.FC<FileRowProps> = ({
onClick={(e) => { e.stopPropagation(); onOpenDialog('delete', node); }}
className="text-destructive focus:text-destructive"
>
<RiDeleteBinLine className="mr-2 h-4 w-4" /> Delete
<RiDeleteBinLine className="mr-2 h-4 w-4" /> {t('sidebarFilesTree.menu.delete')}
</DropdownMenuItem>
</>
)}
@@ -321,6 +323,7 @@ const FileRow: React.FC<FileRowProps> = ({
// --- Main component ---
export const SidebarFilesTree: React.FC = () => {
const { t } = useI18n();
const { files, runtime } = useRuntimeAPIs();
const currentDirectory = useEffectiveDirectory() ?? '';
const root = normalizePath(currentDirectory.trim());
@@ -374,9 +377,9 @@ export const SidebarFilesTree: React.FC = () => {
const handleRevealPath = React.useCallback((targetPath: string) => {
if (!files.revealPath) return;
void files.revealPath(targetPath).catch(() => {
toast.error('Failed to reveal path');
toast.error(t('sidebarFilesTree.toast.revealFailed'));
});
}, [files]);
}, [files, t]);
const handleOpenDialog = React.useCallback((type: 'createFile' | 'createFolder' | 'rename' | 'delete', data: { path: string; name?: string; type?: 'file' | 'directory' }) => {
setActiveDialog(type);
@@ -638,12 +641,12 @@ export const SidebarFilesTree: React.FC = () => {
if (activeDialog === 'createFile') {
if (!dialogInputValue.trim()) {
toast.error('Filename is required');
toast.error(t('sidebarFilesTree.toast.filenameRequired'));
done();
return;
}
if (!files.writeFile) {
toast.error('Write not supported');
toast.error(t('sidebarFilesTree.toast.writeNotSupported'));
done();
return;
}
@@ -655,19 +658,19 @@ export const SidebarFilesTree: React.FC = () => {
await files.writeFile(newPath, '')
.then(async (result) => {
if (result.success) {
toast.success('File created');
toast.success(t('sidebarFilesTree.toast.fileCreated'));
await refreshDirectory(parentPath);
}
closeDialog();
})
.catch(() => toast.error('Operation failed'))
.catch(() => toast.error(t('sidebarFilesTree.toast.operationFailed')))
.finally(done);
return;
}
if (activeDialog === 'createFolder') {
if (!dialogInputValue.trim()) {
toast.error('Folder name is required');
toast.error(t('sidebarFilesTree.toast.folderNameRequired'));
done();
return;
}
@@ -679,24 +682,24 @@ export const SidebarFilesTree: React.FC = () => {
await files.createDirectory(newPath)
.then(async (result) => {
if (result.success) {
toast.success('Folder created');
toast.success(t('sidebarFilesTree.toast.folderCreated'));
await refreshDirectory(parentPath);
}
closeDialog();
})
.catch(() => toast.error('Operation failed'))
.catch(() => toast.error(t('sidebarFilesTree.toast.operationFailed')))
.finally(done);
return;
}
if (activeDialog === 'rename') {
if (!dialogInputValue.trim()) {
toast.error('Name is required');
toast.error(t('sidebarFilesTree.toast.nameRequired'));
done();
return;
}
if (!files.rename) {
toast.error('Rename not supported');
toast.error(t('sidebarFilesTree.toast.renameNotSupported'));
done();
return;
}
@@ -709,7 +712,7 @@ export const SidebarFilesTree: React.FC = () => {
await files.rename(oldPath, newPath)
.then(async (result) => {
if (result.success) {
toast.success('Renamed successfully');
toast.success(t('sidebarFilesTree.toast.renamedSuccessfully'));
await refreshDirectory(parentDir);
if (root) {
removeOpenPathsByPrefix(root, oldPath);
@@ -720,14 +723,14 @@ export const SidebarFilesTree: React.FC = () => {
}
closeDialog();
})
.catch(() => toast.error('Operation failed'))
.catch(() => toast.error(t('sidebarFilesTree.toast.operationFailed')))
.finally(done);
return;
}
if (activeDialog === 'delete') {
if (!files.delete) {
toast.error('Delete not supported');
toast.error(t('sidebarFilesTree.toast.deleteNotSupported'));
done();
return;
}
@@ -737,7 +740,7 @@ export const SidebarFilesTree: React.FC = () => {
await files.delete(deletedPath)
.then(async (result) => {
if (result.success) {
toast.success('Deleted successfully');
toast.success(t('sidebarFilesTree.toast.deletedSuccessfully'));
await refreshDirectory(parentDir);
if (root) {
removeOpenPathsByPrefix(root, deletedPath);
@@ -748,13 +751,13 @@ export const SidebarFilesTree: React.FC = () => {
}
closeDialog();
})
.catch(() => toast.error('Operation failed'))
.catch(() => toast.error(t('sidebarFilesTree.toast.operationFailed')))
.finally(done);
return;
}
done();
}, [activeDialog, dialogData, dialogInputValue, files, refreshDirectory, removeOpenPathsByPrefix, root, selectedPath, setSelectedPath]);
}, [activeDialog, dialogData, dialogInputValue, files, refreshDirectory, removeOpenPathsByPrefix, root, selectedPath, setSelectedPath, t]);
// --- Tree rendering (matching FilesView with indent guides) ---
@@ -814,13 +817,13 @@ export const SidebarFilesTree: React.FC = () => {
ref={searchInputRef}
value={searchQuery}
onChange={(event) => setSearchQuery(event.target.value)}
placeholder="Search files..."
placeholder={t('sidebarFilesTree.search.placeholder')}
className="h-8 pl-8 pr-8 typography-meta"
/>
{searchQuery.trim().length > 0 ? (
<button
type="button"
aria-label="Clear search"
aria-label={t('sidebarFilesTree.search.clearAria')}
className="absolute right-2 top-2 inline-flex h-4 w-4 items-center justify-center text-muted-foreground hover:text-foreground"
onClick={() => {
setSearchQuery('');
@@ -837,7 +840,7 @@ export const SidebarFilesTree: React.FC = () => {
size="sm"
onClick={() => handleOpenDialog('createFile', { path: currentDirectory, type: 'directory' })}
className="h-8 w-8 p-0 flex-shrink-0"
title="New File"
title={t('sidebarFilesTree.actions.newFileTitle')}
>
<RiFileAddLine className="h-4 w-4" />
</Button>
@@ -848,12 +851,12 @@ export const SidebarFilesTree: React.FC = () => {
size="sm"
onClick={() => handleOpenDialog('createFolder', { path: currentDirectory, type: 'directory' })}
className="h-8 w-8 p-0 flex-shrink-0"
title="New Folder"
title={t('sidebarFilesTree.actions.newFolderTitle')}
>
<RiFolderAddLine className="h-4 w-4" />
</Button>
)}
<Button variant="ghost" size="sm" onClick={() => void refreshRoot()} className="h-8 w-8 p-0 flex-shrink-0" title="Refresh">
<Button variant="ghost" size="sm" onClick={() => void refreshRoot()} className="h-8 w-8 p-0 flex-shrink-0" title={t('sidebarFilesTree.actions.refreshTitle')}>
<RiRefreshLine className="h-4 w-4" />
</Button>
</div>
@@ -863,7 +866,7 @@ export const SidebarFilesTree: React.FC = () => {
{searching ? (
<li className="flex items-center gap-1.5 px-2 py-1 typography-meta text-muted-foreground">
<RiLoader4Line className="h-4 w-4 animate-spin" />
Searching...
{t('sidebarFilesTree.state.searching')}
</li>
) : searchResults.length > 0 ? (
searchResults.map((node) => {
@@ -900,7 +903,7 @@ export const SidebarFilesTree: React.FC = () => {
) : hasTree && root ? (
renderTree(root, 0)
) : (
<li className="px-2 py-1 typography-meta text-muted-foreground">Loading...</li>
<li className="px-2 py-1 typography-meta text-muted-foreground">{t('sidebarFilesTree.state.loading')}</li>
)}
</ul>
</ScrollableOverlay>
@@ -910,16 +913,16 @@ export const SidebarFilesTree: React.FC = () => {
<DialogContent>
<DialogHeader>
<DialogTitle>
{activeDialog === 'createFile' && 'Create File'}
{activeDialog === 'createFolder' && 'Create Folder'}
{activeDialog === 'rename' && 'Rename'}
{activeDialog === 'delete' && 'Delete'}
{activeDialog === 'createFile' && t('sidebarFilesTree.dialog.createFile.title')}
{activeDialog === 'createFolder' && t('sidebarFilesTree.dialog.createFolder.title')}
{activeDialog === 'rename' && t('sidebarFilesTree.dialog.rename.title')}
{activeDialog === 'delete' && t('sidebarFilesTree.dialog.delete.title')}
</DialogTitle>
<DialogDescription>
{activeDialog === 'createFile' && `Create a new file in ${dialogData?.path ?? 'root'}`}
{activeDialog === 'createFolder' && `Create a new folder in ${dialogData?.path ?? 'root'}`}
{activeDialog === 'rename' && `Rename ${dialogData?.name}`}
{activeDialog === 'delete' && `Are you sure you want to delete ${dialogData?.name}? This action cannot be undone.`}
{activeDialog === 'createFile' && t('sidebarFilesTree.dialog.createFile.description', { path: dialogData?.path ?? t('sidebarFilesTree.dialog.rootFallback') })}
{activeDialog === 'createFolder' && t('sidebarFilesTree.dialog.createFolder.description', { path: dialogData?.path ?? t('sidebarFilesTree.dialog.rootFallback') })}
{activeDialog === 'rename' && t('sidebarFilesTree.dialog.rename.description', { name: dialogData?.name ?? '' })}
{activeDialog === 'delete' && t('sidebarFilesTree.dialog.delete.description', { name: dialogData?.name ?? '' })}
</DialogDescription>
</DialogHeader>
@@ -928,7 +931,7 @@ export const SidebarFilesTree: React.FC = () => {
<Input
value={dialogInputValue}
onChange={(e) => setDialogInputValue(e.target.value)}
placeholder={activeDialog === 'rename' ? 'New name' : 'Name'}
placeholder={activeDialog === 'rename' ? t('sidebarFilesTree.dialog.rename.placeholder') : t('sidebarFilesTree.dialog.namePlaceholder')}
onKeyDown={(e) => {
if (e.key === 'Enter') {
void handleDialogSubmit();
@@ -941,7 +944,7 @@ export const SidebarFilesTree: React.FC = () => {
<DialogFooter>
<Button variant="outline" onClick={() => setActiveDialog(null)} disabled={isDialogSubmitting}>
Cancel
{t('sidebarFilesTree.dialog.cancel')}
</Button>
<Button
variant={activeDialog === 'delete' ? 'destructive' : 'default'}
@@ -949,7 +952,7 @@ export const SidebarFilesTree: React.FC = () => {
disabled={isDialogSubmitting || (activeDialog !== 'delete' && !dialogInputValue.trim())}
>
{isDialogSubmitting ? <RiLoader4Line className="animate-spin" /> : (
activeDialog === 'delete' ? 'Delete' : 'Confirm'
activeDialog === 'delete' ? t('sidebarFilesTree.dialog.delete.confirm') : t('sidebarFilesTree.dialog.confirm')
)}
</Button>
</DialogFooter>