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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user