feat: added i18n support for Open Project pop-up
This commit is contained in:
@@ -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)} · Updated {formatDate(project.updatedAt)}
|
{t('openProjectModal.meta.created', { date: formatDate(project.createdAt) })} · {t('openProjectModal.meta.updated', { date: formatDate(project.updatedAt) })}
|
||||||
{viewMode === 'trash' && project.deletedAt && (
|
{viewMode === 'trash' && project.deletedAt && (
|
||||||
<> · Deleted {formatDate(project.deletedAt)}</>
|
<> · {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>
|
||||||
|
|||||||
@@ -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}',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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}',
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user