feat: added i18n support for Open Project pop-up

This commit is contained in:
Xiaohan-Tian
2026-05-30 22:18:22 -07:00
parent b5546aa230
commit 4488fe8ecd
3 changed files with 90 additions and 27 deletions
+30 -27
View File
@@ -4,6 +4,8 @@ import { KGProjectStorage, type ProjectMeta } from '../../core/io/KGProjectStora
import { isValidProjectName } from '../../util/projectNameUtil'; import { isValidProjectName } from '../../util/projectNameUtil';
import './OpenProjectModal.css'; import './OpenProjectModal.css';
import { showAlert, showConfirm, showPrompt } from '../../util/dialogUtil'; import { showAlert, showConfirm, showPrompt } from '../../util/dialogUtil';
import { useI18n } from '../../i18n/useI18n';
import { translate } from '../../i18n/translate';
interface OpenProjectModalProps { interface OpenProjectModalProps {
onClose: () => void; onClose: () => void;
@@ -34,7 +36,7 @@ const matchesFilter = (projectName: string, filter: string): boolean => {
}; };
const formatDate = (timestamp: number): string => { const formatDate = (timestamp: number): string => {
if (timestamp === 0) return 'Unknown'; if (timestamp === 0) return translate('openProjectModal.date.unknown');
const date = new Date(timestamp); const date = new Date(timestamp);
return date.toLocaleDateString(undefined, { return date.toLocaleDateString(undefined, {
year: 'numeric', year: 'numeric',
@@ -52,6 +54,7 @@ const OpenProjectModal: React.FC<OpenProjectModalProps> = ({
currentProjectName, currentProjectName,
onCreateNewProject onCreateNewProject
}) => { }) => {
const { t } = useI18n();
const [projects, setProjects] = useState<ProjectMeta[]>([]); const [projects, setProjects] = useState<ProjectMeta[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [filter, setFilter] = useState(''); const [filter, setFilter] = useState('');
@@ -145,12 +148,12 @@ const OpenProjectModal: React.FC<OpenProjectModalProps> = ({
const handleDuplicate = async (e: React.MouseEvent, projectName: string) => { const handleDuplicate = async (e: React.MouseEvent, projectName: string) => {
e.stopPropagation(); e.stopPropagation();
const newName = await showPrompt('Enter a name for the duplicated project:', projectName); const newName = await showPrompt(t('openProjectModal.dialog.duplicateName'), projectName);
if (!newName || newName.trim() === '') return; if (!newName || newName.trim() === '') return;
const trimmed = newName.trim(); const trimmed = newName.trim();
if (!isValidProjectName(trimmed)) { if (!isValidProjectName(trimmed)) {
await showAlert('Invalid project name. Only letters, numbers, spaces, hyphens, underscores, periods, and parentheses are allowed.'); await showAlert(t('openProjectModal.dialog.invalidName'));
return; return;
} }
@@ -165,7 +168,7 @@ const OpenProjectModal: React.FC<OpenProjectModalProps> = ({
startClose(); startClose();
} catch (error) { } catch (error) {
console.error('Error duplicating project:', error); console.error('Error duplicating project:', error);
await showAlert(`Failed to duplicate project: ${error}`); await showAlert(t('openProjectModal.dialog.duplicateError', { error: String(error) }));
} }
}; };
@@ -177,7 +180,7 @@ const OpenProjectModal: React.FC<OpenProjectModalProps> = ({
await fetchProjects(viewMode); await fetchProjects(viewMode);
} catch (error) { } catch (error) {
console.error('Error deleting project:', error); console.error('Error deleting project:', error);
await showAlert(`Failed to delete project: ${error}`); await showAlert(t('openProjectModal.dialog.deleteError', { error: String(error) }));
} }
}; };
@@ -189,14 +192,14 @@ const OpenProjectModal: React.FC<OpenProjectModalProps> = ({
await fetchProjects(viewMode); await fetchProjects(viewMode);
} catch (error) { } catch (error) {
console.error('Error restoring project:', error); console.error('Error restoring project:', error);
await showAlert(`Failed to restore project: ${error}`); await showAlert(t('openProjectModal.dialog.restoreError', { error: String(error) }));
} }
}; };
const handlePermanentDelete = async (e: React.MouseEvent, projectName: string) => { const handlePermanentDelete = async (e: React.MouseEvent, projectName: string) => {
e.stopPropagation(); e.stopPropagation();
const confirmed = await showConfirm( const confirmed = await showConfirm(
`Are you sure you want to permanently delete "${projectName}"?\n\nThis operation cannot be undone.` t('openProjectModal.dialog.confirmPermanentDelete', { name: projectName })
); );
if (!confirmed) return; if (!confirmed) return;
@@ -210,7 +213,7 @@ const OpenProjectModal: React.FC<OpenProjectModalProps> = ({
} }
} catch (error) { } catch (error) {
console.error('Error permanently deleting project:', error); console.error('Error permanently deleting project:', error);
await showAlert(`Failed to permanently delete project: ${error}`); await showAlert(t('openProjectModal.dialog.permanentDeleteError', { error: String(error) }));
} }
}; };
@@ -243,8 +246,8 @@ const OpenProjectModal: React.FC<OpenProjectModalProps> = ({
> >
<div className={`open-project-panel${isClosing ? ' open-project-panel-closing' : ''}`}> <div className={`open-project-panel${isClosing ? ' open-project-panel-closing' : ''}`}>
<div className="open-project-header"> <div className="open-project-header">
<h3 className="open-project-title">Open Project</h3> <h3 className="open-project-title">{t('openProjectModal.title')}</h3>
<button className="open-project-close-btn" onClick={startClose} aria-label="Close"> <button className="open-project-close-btn" onClick={startClose} aria-label={t('openProjectModal.close')}>
<FaTimes /> <FaTimes />
</button> </button>
</div> </div>
@@ -253,7 +256,7 @@ const OpenProjectModal: React.FC<OpenProjectModalProps> = ({
<div className="open-project-filter-row"> <div className="open-project-filter-row">
<input <input
type="text" type="text"
placeholder="Filter projects..." placeholder={t('openProjectModal.filter.placeholder')}
value={filter} value={filter}
onChange={(e) => setFilter(e.target.value)} onChange={(e) => setFilter(e.target.value)}
autoFocus autoFocus
@@ -263,13 +266,13 @@ const OpenProjectModal: React.FC<OpenProjectModalProps> = ({
className={`open-project-toggle-btn ${viewMode === 'projects' ? 'active' : ''}`} className={`open-project-toggle-btn ${viewMode === 'projects' ? 'active' : ''}`}
onClick={() => handleViewModeChange('projects')} onClick={() => handleViewModeChange('projects')}
> >
Projects {t('openProjectModal.viewToggle.projects')}
</button> </button>
<button <button
className={`open-project-toggle-btn ${viewMode === 'trash' ? 'active' : ''}`} className={`open-project-toggle-btn ${viewMode === 'trash' ? 'active' : ''}`}
onClick={() => handleViewModeChange('trash')} onClick={() => handleViewModeChange('trash')}
> >
Trash {t('openProjectModal.viewToggle.trash')}
</button> </button>
</div> </div>
</div> </div>
@@ -277,7 +280,7 @@ const OpenProjectModal: React.FC<OpenProjectModalProps> = ({
{viewMode === 'trash' && ( {viewMode === 'trash' && (
<div className="open-project-trash-hint"> <div className="open-project-trash-hint">
Deleted projects are automatically removed after 30 days. {t('openProjectModal.trash.hint')}
</div> </div>
)} )}
@@ -286,29 +289,29 @@ const OpenProjectModal: React.FC<OpenProjectModalProps> = ({
className={`open-project-sort-col open-project-sort-col-name ${sortField === 'name' ? 'active' : ''}`} className={`open-project-sort-col open-project-sort-col-name ${sortField === 'name' ? 'active' : ''}`}
onClick={() => handleSortClick('name')} onClick={() => handleSortClick('name')}
> >
Name <SortIcon field="name" /> {t('openProjectModal.sort.name')} <SortIcon field="name" />
</span> </span>
<span <span
className={`open-project-sort-col open-project-sort-col-created ${sortField === 'createdAt' ? 'active' : ''}`} className={`open-project-sort-col open-project-sort-col-created ${sortField === 'createdAt' ? 'active' : ''}`}
onClick={() => handleSortClick('createdAt')} onClick={() => handleSortClick('createdAt')}
> >
Created <SortIcon field="createdAt" /> {t('openProjectModal.sort.created')} <SortIcon field="createdAt" />
</span> </span>
<span <span
className={`open-project-sort-col open-project-sort-col-updated ${sortField === 'updatedAt' ? 'active' : ''}`} className={`open-project-sort-col open-project-sort-col-updated ${sortField === 'updatedAt' ? 'active' : ''}`}
onClick={() => handleSortClick('updatedAt')} onClick={() => handleSortClick('updatedAt')}
> >
Updated <SortIcon field="updatedAt" /> {t('openProjectModal.sort.updated')} <SortIcon field="updatedAt" />
</span> </span>
</div> </div>
{isLoading ? ( {isLoading ? (
<div className="open-project-loading">Loading projects...</div> <div className="open-project-loading">{t('openProjectModal.loading')}</div>
) : filteredProjects.length === 0 ? ( ) : filteredProjects.length === 0 ? (
<div className="open-project-empty"> <div className="open-project-empty">
{projects.length === 0 {projects.length === 0
? (viewMode === 'trash' ? 'Trash is empty' : 'No saved projects') ? (viewMode === 'trash' ? t('openProjectModal.empty.trash') : t('openProjectModal.empty.noProjects'))
: 'No projects match the filter'} : t('openProjectModal.empty.noMatch')}
</div> </div>
) : ( ) : (
<div className="open-project-list"> <div className="open-project-list">
@@ -321,9 +324,9 @@ const OpenProjectModal: React.FC<OpenProjectModalProps> = ({
<div className="open-project-item-info"> <div className="open-project-item-info">
<div className="open-project-item-name">{project.name}</div> <div className="open-project-item-name">{project.name}</div>
<div className="open-project-item-meta"> <div className="open-project-item-meta">
Created {formatDate(project.createdAt)} &middot; Updated {formatDate(project.updatedAt)} {t('openProjectModal.meta.created', { date: formatDate(project.createdAt) })} &middot; {t('openProjectModal.meta.updated', { date: formatDate(project.updatedAt) })}
{viewMode === 'trash' && project.deletedAt && ( {viewMode === 'trash' && project.deletedAt && (
<> &middot; Deleted {formatDate(project.deletedAt)}</> <> &middot; {t('openProjectModal.meta.deleted', { date: formatDate(project.deletedAt) })}</>
)} )}
</div> </div>
</div> </div>
@@ -334,19 +337,19 @@ const OpenProjectModal: React.FC<OpenProjectModalProps> = ({
className="open-project-item-open-btn" className="open-project-item-open-btn"
onClick={(e) => { e.stopPropagation(); handleOpen(project.name); }} onClick={(e) => { e.stopPropagation(); handleOpen(project.name); }}
> >
Open {t('openProjectModal.action.open')}
</button> </button>
<button <button
className="open-project-item-action-btn open-project-item-duplicate-btn" className="open-project-item-action-btn open-project-item-duplicate-btn"
onClick={(e) => handleDuplicate(e, project.name)} onClick={(e) => handleDuplicate(e, project.name)}
title="Duplicate" title={t('openProjectModal.action.duplicate')}
> >
<FaCopy /> <FaCopy />
</button> </button>
<button <button
className="open-project-item-action-btn open-project-item-delete-btn" className="open-project-item-action-btn open-project-item-delete-btn"
onClick={(e) => handleSoftDelete(e, project.name)} onClick={(e) => handleSoftDelete(e, project.name)}
title="Delete" title={t('openProjectModal.action.delete')}
> >
<FaTrash /> <FaTrash />
</button> </button>
@@ -356,14 +359,14 @@ const OpenProjectModal: React.FC<OpenProjectModalProps> = ({
<button <button
className="open-project-item-action-btn open-project-item-restore-btn" className="open-project-item-action-btn open-project-item-restore-btn"
onClick={(e) => handleRestore(e, project.name)} onClick={(e) => handleRestore(e, project.name)}
title="Restore" title={t('openProjectModal.action.restore')}
> >
<FaUndo /> <FaUndo />
</button> </button>
<button <button
className="open-project-item-action-btn open-project-item-permdelete-btn" className="open-project-item-action-btn open-project-item-permdelete-btn"
onClick={(e) => handlePermanentDelete(e, project.name)} onClick={(e) => handlePermanentDelete(e, project.name)}
title="Delete permanently" title={t('openProjectModal.action.deletePermanently')}
> >
<FaTrash /> <FaTrash />
</button> </button>
+30
View File
@@ -659,4 +659,34 @@ export const enUsMessages: TranslationMessages = {
'kgone.separator.local.progress.readingAudio': 'Reading audio file...', 'kgone.separator.local.progress.readingAudio': 'Reading audio file...',
'kgone.separator.local.progress.running': 'Running browser separation...', 'kgone.separator.local.progress.running': 'Running browser separation...',
'kgone.separator.local.progress.complete': 'Separation complete.', 'kgone.separator.local.progress.complete': 'Separation complete.',
'openProjectModal.title': 'Open Project',
'openProjectModal.close': 'Close',
'openProjectModal.loading': 'Loading projects...',
'openProjectModal.date.unknown': 'Unknown',
'openProjectModal.filter.placeholder': 'Filter projects...',
'openProjectModal.viewToggle.projects': 'Projects',
'openProjectModal.viewToggle.trash': 'Trash',
'openProjectModal.trash.hint': 'Deleted projects are automatically removed after 30 days.',
'openProjectModal.sort.name': 'Name',
'openProjectModal.sort.created': 'Created',
'openProjectModal.sort.updated': 'Updated',
'openProjectModal.empty.trash': 'Trash is empty',
'openProjectModal.empty.noProjects': 'No saved projects',
'openProjectModal.empty.noMatch': 'No projects match the filter',
'openProjectModal.meta.created': 'Created {date}',
'openProjectModal.meta.updated': 'Updated {date}',
'openProjectModal.meta.deleted': 'Deleted {date}',
'openProjectModal.action.open': 'Open',
'openProjectModal.action.duplicate': 'Duplicate',
'openProjectModal.action.delete': 'Delete',
'openProjectModal.action.restore': 'Restore',
'openProjectModal.action.deletePermanently': 'Delete permanently',
'openProjectModal.dialog.duplicateName': 'Enter a name for the duplicated project:',
'openProjectModal.dialog.invalidName': 'Invalid project name. Only letters, numbers, spaces, hyphens, underscores, periods, and parentheses are allowed.',
'openProjectModal.dialog.duplicateError': 'Failed to duplicate project: {error}',
'openProjectModal.dialog.deleteError': 'Failed to delete project: {error}',
'openProjectModal.dialog.restoreError': 'Failed to restore project: {error}',
'openProjectModal.dialog.confirmPermanentDelete': 'Are you sure you want to permanently delete "{name}"?\n\nThis operation cannot be undone.',
'openProjectModal.dialog.permanentDeleteError': 'Failed to permanently delete project: {error}',
}; };
+30
View File
@@ -659,4 +659,34 @@ export const zhCnMessages: TranslationMessages = {
'kgone.separator.local.progress.readingAudio': '正在读取音频文件...', 'kgone.separator.local.progress.readingAudio': '正在读取音频文件...',
'kgone.separator.local.progress.running': '正在浏览器中执行分离...', 'kgone.separator.local.progress.running': '正在浏览器中执行分离...',
'kgone.separator.local.progress.complete': '分离完成。', 'kgone.separator.local.progress.complete': '分离完成。',
'openProjectModal.title': '打开项目',
'openProjectModal.close': '关闭',
'openProjectModal.loading': '正在加载项目...',
'openProjectModal.date.unknown': '未知',
'openProjectModal.filter.placeholder': '筛选项目...',
'openProjectModal.viewToggle.projects': '项目',
'openProjectModal.viewToggle.trash': '回收站',
'openProjectModal.trash.hint': '已删除的项目将在 30 天后自动移除。',
'openProjectModal.sort.name': '名称',
'openProjectModal.sort.created': '创建时间',
'openProjectModal.sort.updated': '更新时间',
'openProjectModal.empty.trash': '回收站为空',
'openProjectModal.empty.noProjects': '暂无已保存的项目',
'openProjectModal.empty.noMatch': '没有符合筛选条件的项目',
'openProjectModal.meta.created': '创建于 {date}',
'openProjectModal.meta.updated': '更新于 {date}',
'openProjectModal.meta.deleted': '删除于 {date}',
'openProjectModal.action.open': '打开',
'openProjectModal.action.duplicate': '复制',
'openProjectModal.action.delete': '删除',
'openProjectModal.action.restore': '恢复',
'openProjectModal.action.deletePermanently': '永久删除',
'openProjectModal.dialog.duplicateName': '请输入复制项目的名称:',
'openProjectModal.dialog.invalidName': '项目名称无效。只允许使用字母、数字、空格、连字符、下划线、句号和括号。',
'openProjectModal.dialog.duplicateError': '复制项目失败:{error}',
'openProjectModal.dialog.deleteError': '删除项目失败:{error}',
'openProjectModal.dialog.restoreError': '恢复项目失败:{error}',
'openProjectModal.dialog.confirmPermanentDelete': '确定要永久删除"{name}"吗?\n\n此操作无法撤销。',
'openProjectModal.dialog.permanentDeleteError': '永久删除项目失败:{error}',
}; };