chore: more i18n going on
This commit is contained in:
@@ -1,9 +0,0 @@
|
||||
## JSX text nodes (tsx)
|
||||
|
||||
## UI props with literal text
|
||||
|
||||
## Dialogs/prompts/alerts/confirms
|
||||
|
||||
## Toasts and notifications with literals
|
||||
|
||||
## Main-process menu/dialog labels
|
||||
@@ -358,8 +358,8 @@ export const PostEditor: React.FC<PostEditorProps> = ({ postId }) => {
|
||||
console.error('Failed to save post:', error);
|
||||
const err = error as Error;
|
||||
showErrorModal({
|
||||
title: 'Save Failed',
|
||||
message: err.message || 'Failed to save post',
|
||||
title: tr('editor.error.saveTitle'),
|
||||
message: err.message || tr('editor.error.saveMessage'),
|
||||
stack: err.stack,
|
||||
});
|
||||
} finally {
|
||||
@@ -374,14 +374,14 @@ export const PostEditor: React.FC<PostEditorProps> = ({ postId }) => {
|
||||
if (updated) {
|
||||
updatePost(postId, updated as Partial<PostData>);
|
||||
setPost(prev => prev ? { ...prev, ...updated as Partial<PostData> } : prev);
|
||||
showToast.success('Post published');
|
||||
showToast.success(tr('editor.toast.published'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to publish post:', error);
|
||||
const err = error as Error;
|
||||
showErrorModal({
|
||||
title: 'Publish Failed',
|
||||
message: err.message || 'Failed to publish post',
|
||||
title: tr('editor.error.publishTitle'),
|
||||
message: err.message || tr('editor.error.publishMessage'),
|
||||
stack: err.stack,
|
||||
});
|
||||
}
|
||||
@@ -391,8 +391,8 @@ export const PostEditor: React.FC<PostEditorProps> = ({ postId }) => {
|
||||
// If this post has a published version, revert to it
|
||||
// If never published, delete the post entirely
|
||||
const confirmMessage = hasPublishedVersion
|
||||
? 'Discard all changes since last publish? This cannot be undone.'
|
||||
: 'Delete this draft? This cannot be undone.';
|
||||
? tr('editor.confirm.discardChanges')
|
||||
: tr('editor.confirm.deleteDraft');
|
||||
|
||||
if (!confirm(confirmMessage)) {
|
||||
return;
|
||||
@@ -412,7 +412,7 @@ export const PostEditor: React.FC<PostEditorProps> = ({ postId }) => {
|
||||
setPost(reverted as PostData);
|
||||
updatePost(postId, reverted as Partial<PostData>);
|
||||
markClean(postId);
|
||||
showToast.success('Reverted to last published version');
|
||||
showToast.success(tr('editor.toast.reverted'));
|
||||
}
|
||||
} else {
|
||||
// Never published - delete the post entirely
|
||||
@@ -421,14 +421,14 @@ export const PostEditor: React.FC<PostEditorProps> = ({ postId }) => {
|
||||
pendingChangesRef.current = null;
|
||||
useAppStore.getState().removePost(postId);
|
||||
useAppStore.getState().closeTab(postId);
|
||||
showToast.success('Draft deleted');
|
||||
showToast.success(tr('editor.toast.draftDeleted'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to discard/delete:', error);
|
||||
const err = error as Error;
|
||||
showErrorModal({
|
||||
title: hasPublishedVersion ? 'Discard Failed' : 'Delete Failed',
|
||||
message: err.message || 'Operation failed',
|
||||
title: hasPublishedVersion ? tr('editor.error.discardTitle') : tr('editor.error.deleteTitle'),
|
||||
message: err.message || tr('editor.error.operationMessage'),
|
||||
stack: err.stack,
|
||||
});
|
||||
}
|
||||
@@ -469,7 +469,7 @@ export const PostEditor: React.FC<PostEditorProps> = ({ postId }) => {
|
||||
// Show confirmation modal
|
||||
showConfirmDeleteModal({
|
||||
itemType: 'post',
|
||||
itemTitle: title || 'Untitled',
|
||||
itemTitle: title || tr('editor.untitled'),
|
||||
references,
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
@@ -479,13 +479,13 @@ export const PostEditor: React.FC<PostEditorProps> = ({ postId }) => {
|
||||
useAppStore.getState().removePost(postId);
|
||||
useAppStore.getState().closeTab(postId);
|
||||
useAppStore.getState().setSelectedPost(null);
|
||||
showToast.success('Post deleted');
|
||||
showToast.success(tr('editor.toast.postDeleted'));
|
||||
} catch (error) {
|
||||
console.error('Failed to delete post:', error);
|
||||
const err = error as Error;
|
||||
showErrorModal({
|
||||
title: 'Delete Failed',
|
||||
message: err.message || 'Failed to delete post',
|
||||
title: tr('editor.error.deleteTitle'),
|
||||
message: err.message || tr('editor.error.deletePostMessage'),
|
||||
stack: err.stack,
|
||||
});
|
||||
}
|
||||
@@ -495,8 +495,8 @@ export const PostEditor: React.FC<PostEditorProps> = ({ postId }) => {
|
||||
console.error('Failed to fetch post references:', error);
|
||||
const err = error as Error;
|
||||
showErrorModal({
|
||||
title: 'Error',
|
||||
message: err.message || 'Failed to fetch post references',
|
||||
title: tr('errorModal.error'),
|
||||
message: err.message || tr('editor.error.fetchPostReferencesMessage'),
|
||||
stack: err.stack,
|
||||
});
|
||||
}
|
||||
@@ -930,6 +930,7 @@ export const PostEditor: React.FC<PostEditorProps> = ({ postId }) => {
|
||||
};
|
||||
|
||||
const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
const { t: tr } = useI18n();
|
||||
const { media, updateMedia, showErrorModal, showConfirmDeleteModal, openTab } = useAppStore();
|
||||
const item = media.find(m => m.id === mediaId);
|
||||
|
||||
@@ -997,11 +998,11 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
caption: result.caption,
|
||||
});
|
||||
} else {
|
||||
setAIError(result?.error || 'Failed to analyze image');
|
||||
setAIError(result?.error || tr('editor.media.error.analyzeImage'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to analyze image:', error);
|
||||
setAIError((error as Error).message || 'Failed to analyze image');
|
||||
setAIError((error as Error).message || tr('editor.media.error.analyzeImage'));
|
||||
} finally {
|
||||
setIsAnalyzing(false);
|
||||
}
|
||||
@@ -1014,7 +1015,7 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
if (values.caption) setCaption(values.caption);
|
||||
setShowAISuggestionsModal(false);
|
||||
if (Object.keys(values).length > 0) {
|
||||
showToast.success('AI suggestions applied');
|
||||
showToast.success(tr('editor.media.toast.aiApplied'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1038,7 +1039,7 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
for (const link of links) {
|
||||
const post = await window.electronAPI?.posts.get(link.postId);
|
||||
if (post) {
|
||||
titles.set(link.postId, post.title || 'Untitled');
|
||||
titles.set(link.postId, post.title || tr('editor.untitled'));
|
||||
}
|
||||
}
|
||||
setPostTitles(titles);
|
||||
@@ -1057,7 +1058,7 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
try {
|
||||
const result = await window.electronAPI?.posts.getAll({ limit: 100, offset: 0 });
|
||||
if (result?.items) {
|
||||
setPickerPosts(result.items.map(p => ({ id: p.id, title: p.title || 'Untitled' })));
|
||||
setPickerPosts(result.items.map(p => ({ id: p.id, title: p.title || tr('editor.untitled') })));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load posts for picker:', error);
|
||||
@@ -1068,7 +1069,7 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
|
||||
// Get post titles for display
|
||||
const getPostTitle = (postId: string): string => {
|
||||
return postTitles.get(postId) || 'Loading...';
|
||||
return postTitles.get(postId) || tr('sidebar.loading');
|
||||
};
|
||||
|
||||
// Handle linking to a new post
|
||||
@@ -1079,10 +1080,10 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
setPostTitles(prev => new Map(prev).set(postId, postTitle));
|
||||
setShowPostPicker(false);
|
||||
setPostSearchQuery('');
|
||||
showToast.success('Linked to post');
|
||||
showToast.success(tr('editor.media.toast.linkedToPost'));
|
||||
} catch (error) {
|
||||
console.error('Failed to link to post:', error);
|
||||
showToast.error('Failed to link to post');
|
||||
showToast.error(tr('editor.media.toast.linkFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1091,10 +1092,10 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
try {
|
||||
await window.electronAPI?.postMedia.unlink(postId, mediaId);
|
||||
setLinkedPosts(linkedPosts.filter(l => l.postId !== postId));
|
||||
showToast.success('Unlinked from post');
|
||||
showToast.success(tr('editor.media.toast.unlinkedFromPost'));
|
||||
} catch (error) {
|
||||
console.error('Failed to unlink from post:', error);
|
||||
showToast.error('Failed to unlink from post');
|
||||
showToast.error(tr('editor.media.toast.unlinkFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1121,7 +1122,7 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
}, [item?.id]);
|
||||
|
||||
if (!item) {
|
||||
return <div className="editor-empty">Media not found</div>;
|
||||
return <div className="editor-empty">{tr('editor.media.notFound')}</div>;
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
@@ -1135,14 +1136,14 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
});
|
||||
if (updated) {
|
||||
updateMedia(item.id, updated as Partial<typeof item>);
|
||||
showToast.success('Media updated');
|
||||
showToast.success(tr('editor.media.toast.updated'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update media:', error);
|
||||
const err = error as Error;
|
||||
showErrorModal({
|
||||
title: 'Update Failed',
|
||||
message: err.message || 'Failed to update media',
|
||||
title: tr('editor.media.error.updateTitle'),
|
||||
message: err.message || tr('editor.media.error.updateMessage'),
|
||||
stack: err.stack,
|
||||
});
|
||||
}
|
||||
@@ -1153,15 +1154,15 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
const updated = await window.electronAPI?.media.replaceFileDialog(item.id);
|
||||
if (updated) {
|
||||
updateMedia(item.id, updated as Partial<typeof item>);
|
||||
showToast.success('File replaced (thumbnails regenerated)');
|
||||
showToast.success(tr('editor.media.toast.fileReplaced'));
|
||||
}
|
||||
// null means user cancelled or file unchanged - no action needed
|
||||
} catch (error) {
|
||||
console.error('Failed to replace media file:', error);
|
||||
const err = error as Error;
|
||||
showErrorModal({
|
||||
title: 'Replace Failed',
|
||||
message: err.message || 'Failed to replace media file',
|
||||
title: tr('editor.media.error.replaceTitle'),
|
||||
message: err.message || tr('editor.media.error.replaceMessage'),
|
||||
stack: err.stack,
|
||||
});
|
||||
}
|
||||
@@ -1182,7 +1183,7 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
if (post) {
|
||||
references.push({
|
||||
id: post.id,
|
||||
title: post.title || 'Untitled',
|
||||
title: post.title || tr('editor.untitled'),
|
||||
type: 'post',
|
||||
});
|
||||
}
|
||||
@@ -1198,13 +1199,13 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
try {
|
||||
await window.electronAPI?.media.delete(item.id);
|
||||
useAppStore.getState().removeMedia(item.id);
|
||||
showToast.success('Media deleted');
|
||||
showToast.success(tr('editor.media.toast.deleted'));
|
||||
} catch (error) {
|
||||
console.error('Failed to delete media:', error);
|
||||
const err = error as Error;
|
||||
showErrorModal({
|
||||
title: 'Delete Failed',
|
||||
message: err.message || 'Failed to delete media',
|
||||
title: tr('editor.error.deleteTitle'),
|
||||
message: err.message || tr('editor.media.error.deleteMessage'),
|
||||
stack: err.stack,
|
||||
});
|
||||
}
|
||||
@@ -1214,8 +1215,8 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
console.error('Failed to fetch media references:', error);
|
||||
const err = error as Error;
|
||||
showErrorModal({
|
||||
title: 'Error',
|
||||
message: err.message || 'Failed to fetch media references',
|
||||
title: tr('errorModal.error'),
|
||||
message: err.message || tr('editor.media.error.fetchReferencesMessage'),
|
||||
stack: err.stack,
|
||||
});
|
||||
}
|
||||
@@ -1237,9 +1238,9 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
className="secondary quick-actions-btn"
|
||||
onClick={() => setShowQuickActions(!showQuickActions)}
|
||||
disabled={isAnalyzing}
|
||||
title="Quick Actions"
|
||||
title={tr('editor.media.quickActions.title')}
|
||||
>
|
||||
{isAnalyzing ? '⏳ Analyzing...' : '⚡ Quick Actions'}
|
||||
{isAnalyzing ? tr('editor.media.quickActions.analyzing') : tr('editor.media.quickActions.button')}
|
||||
</button>
|
||||
{showQuickActions && (
|
||||
<div className="quick-actions-menu">
|
||||
@@ -1250,17 +1251,17 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
>
|
||||
<span className="quick-action-icon">🤖</span>
|
||||
<span className="quick-action-text">
|
||||
<strong>AI: Generate Title, Alt & Caption</strong>
|
||||
<small>Analyzes the image to suggest metadata</small>
|
||||
<strong>{tr('editor.media.quickActions.aiTitle')}</strong>
|
||||
<small>{tr('editor.media.quickActions.aiDescription')}</small>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<button onClick={handleReplaceFile} className="secondary">Replace File</button>
|
||||
<button onClick={handleSave}>Save</button>
|
||||
<button onClick={handleDelete} className="secondary danger">Delete</button>
|
||||
<button onClick={handleReplaceFile} className="secondary">{tr('editor.media.replaceFile')}</button>
|
||||
<button onClick={handleSave}>{tr('common.save')}</button>
|
||||
<button onClick={handleDelete} className="secondary danger">{tr('editor.delete')}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1291,81 +1292,81 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
|
||||
<div className="media-details">
|
||||
<div className="editor-field">
|
||||
<label>File Name</label>
|
||||
<label>{tr('editor.media.field.fileName')}</label>
|
||||
<input type="text" value={item.originalName} disabled className="disabled" />
|
||||
</div>
|
||||
<div className="editor-field">
|
||||
<label>Type</label>
|
||||
<label>{tr('editor.media.field.type')}</label>
|
||||
<input type="text" value={item.mimeType} disabled className="disabled" />
|
||||
</div>
|
||||
<div className="editor-field-row">
|
||||
<div className="editor-field">
|
||||
<label>Size</label>
|
||||
<label>{tr('editor.media.field.size')}</label>
|
||||
<input type="text" value={`${(item.size / 1024).toFixed(1)} KB`} disabled className="disabled" />
|
||||
</div>
|
||||
{item.width && item.height && (
|
||||
<div className="editor-field">
|
||||
<label>Dimensions</label>
|
||||
<label>{tr('editor.media.field.dimensions')}</label>
|
||||
<input type="text" value={`${item.width} × ${item.height}`} disabled className="disabled" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="editor-field">
|
||||
<label>Title</label>
|
||||
<label>{tr('editor.media.field.title')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Title for lists and search results"
|
||||
placeholder={tr('editor.media.placeholder.title')}
|
||||
/>
|
||||
</div>
|
||||
<div className="editor-field">
|
||||
<label>Alt Text</label>
|
||||
<label>{tr('editor.media.field.altText')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={alt}
|
||||
onChange={(e) => setAlt(e.target.value)}
|
||||
placeholder="Describe the image for accessibility"
|
||||
placeholder={tr('editor.media.placeholder.altText')}
|
||||
/>
|
||||
</div>
|
||||
<div className="editor-field">
|
||||
<label>Caption</label>
|
||||
<label>{tr('editor.media.field.caption')}</label>
|
||||
<textarea
|
||||
value={caption}
|
||||
onChange={(e) => setCaption(e.target.value)}
|
||||
placeholder="Image caption"
|
||||
placeholder={tr('editor.media.placeholder.caption')}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<div className="editor-field">
|
||||
<label>Tags (comma-separated)</label>
|
||||
<label>{tr('editor.media.field.tags')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={tags}
|
||||
onChange={(e) => setTags(e.target.value)}
|
||||
placeholder="tag1, tag2, tag3"
|
||||
placeholder={tr('editor.media.placeholder.tags')}
|
||||
/>
|
||||
</div>
|
||||
<div className="editor-field">
|
||||
<label>Author</label>
|
||||
<label>{tr('editor.media.field.author')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={author}
|
||||
onChange={(e) => setAuthor(e.target.value)}
|
||||
placeholder="Author name"
|
||||
placeholder={tr('editor.media.placeholder.author')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Linked Posts Section */}
|
||||
<div className="editor-field linked-posts-section">
|
||||
<label>
|
||||
Linked Posts
|
||||
{tr('editor.media.linkedPosts')}
|
||||
<button
|
||||
className="add-link-btn"
|
||||
onClick={() => setShowPostPicker(!showPostPicker)}
|
||||
title="Link to a post"
|
||||
title={tr('editor.media.linkToPostTitle')}
|
||||
>
|
||||
+ Link
|
||||
{tr('editor.media.linkAction')}
|
||||
</button>
|
||||
</label>
|
||||
|
||||
@@ -1374,14 +1375,14 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
<div className="post-picker-search">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search posts..."
|
||||
placeholder={tr('editor.media.searchPosts')}
|
||||
value={postSearchQuery}
|
||||
onChange={(e) => setPostSearchQuery(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
{unlinkedPosts.length === 0 ? (
|
||||
<div className="no-posts">{postSearchQuery ? 'No matching posts' : 'No posts available to link'}</div>
|
||||
<div className="no-posts">{postSearchQuery ? tr('editor.media.noMatchingPosts') : tr('editor.media.noPostsToLink')}</div>
|
||||
) : (
|
||||
<div className="post-picker-list">
|
||||
{unlinkedPosts.slice(0, 10).map(post => (
|
||||
@@ -1395,7 +1396,7 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
))}
|
||||
{unlinkedPosts.length > 10 && (
|
||||
<div className="post-picker-more">
|
||||
+{unlinkedPosts.length - 10} more posts
|
||||
{tr('editor.media.morePosts', { count: unlinkedPosts.length - 10 })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -1404,7 +1405,7 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
)}
|
||||
|
||||
{linkedPosts.length === 0 ? (
|
||||
<div className="no-linked-posts">Not linked to any posts</div>
|
||||
<div className="no-linked-posts">{tr('editor.media.notLinked')}</div>
|
||||
) : (
|
||||
<div className="linked-posts-list">
|
||||
{linkedPosts.map(({ postId }) => (
|
||||
@@ -1412,14 +1413,14 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
<span
|
||||
className="linked-post-title"
|
||||
onClick={() => handlePostClick(postId)}
|
||||
title="Open post"
|
||||
title={tr('editor.media.openPost')}
|
||||
>
|
||||
📄 {getPostTitle(postId)}
|
||||
</span>
|
||||
<button
|
||||
className="unlink-btn"
|
||||
onClick={() => handleUnlinkFromPost(postId)}
|
||||
title="Unlink from post"
|
||||
title={tr('editor.media.unlinkFromPost')}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import type { ChatModel } from '../../types/electron';
|
||||
import { useI18n } from '../../i18n';
|
||||
import './ImportAnalysisView.css';
|
||||
|
||||
/** How to resolve a slug conflict during import */
|
||||
@@ -155,7 +156,8 @@ const formatEta = (etaMs: number): string => {
|
||||
};
|
||||
|
||||
export const ImportAnalysisView: React.FC<ImportAnalysisViewProps> = ({ definitionId }) => {
|
||||
const [name, setName] = useState('Untitled Import');
|
||||
const { t } = useI18n();
|
||||
const [name, setName] = useState(t('importAnalysis.untitledImport'));
|
||||
const [uploadsFolder, setUploadsFolder] = useState<string | null>(null);
|
||||
const [wxrFilePath, setWxrFilePath] = useState<string | null>(null);
|
||||
const [report, setReport] = useState<AnalysisReport | null>(null);
|
||||
@@ -333,7 +335,7 @@ export const ImportAnalysisView: React.FC<ImportAnalysisViewProps> = ({ definiti
|
||||
}, [definitionId]);
|
||||
|
||||
const handleNameBlur = useCallback(async () => {
|
||||
const trimmed = name.trim() || 'Untitled Import';
|
||||
const trimmed = name.trim() || t('importAnalysis.untitledImport');
|
||||
setName(trimmed);
|
||||
await window.electronAPI?.importDefinitions.update(definitionId, { name: trimmed });
|
||||
}, [definitionId, name]);
|
||||
@@ -378,7 +380,7 @@ export const ImportAnalysisView: React.FC<ImportAnalysisViewProps> = ({ definiti
|
||||
setExecutionState({
|
||||
isExecuting: true,
|
||||
taskId: null,
|
||||
phase: 'Starting...',
|
||||
phase: t('importAnalysis.executionStarting'),
|
||||
current: 0,
|
||||
total: 0,
|
||||
detail: '',
|
||||
@@ -405,7 +407,7 @@ export const ImportAnalysisView: React.FC<ImportAnalysisViewProps> = ({ definiti
|
||||
setExecutionState(prev => ({
|
||||
...prev,
|
||||
isExecuting: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
error: error instanceof Error ? error.message : t('importAnalysis.unknownError'),
|
||||
}));
|
||||
}
|
||||
}, [report, uploadsFolder]);
|
||||
@@ -478,7 +480,7 @@ export const ImportAnalysisView: React.FC<ImportAnalysisViewProps> = ({ definiti
|
||||
<div className="import-analysis">
|
||||
<div className="import-loading">
|
||||
<div className="import-spinner" />
|
||||
Loading import definition...
|
||||
{t('importAnalysis.loadingDefinition')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -494,30 +496,30 @@ export const ImportAnalysisView: React.FC<ImportAnalysisViewProps> = ({ definiti
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onBlur={handleNameBlur}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') nameInputRef.current?.blur(); }}
|
||||
placeholder="Import name..."
|
||||
placeholder={t('importAnalysis.namePlaceholder')}
|
||||
/>
|
||||
<p>Select a WordPress export file (WXR) and an uploads folder to analyze what would be imported.</p>
|
||||
<p>{t('importAnalysis.headerDescription')}</p>
|
||||
</div>
|
||||
|
||||
<div className="import-file-selectors">
|
||||
<div className="import-file-row">
|
||||
<label>Uploads Folder</label>
|
||||
<label>{t('importAnalysis.uploadsFolder')}</label>
|
||||
<div className={`import-file-path ${!uploadsFolder ? 'placeholder' : ''}`}>
|
||||
{uploadsFolder || 'No folder selected'}
|
||||
{uploadsFolder || t('importAnalysis.noFolderSelected')}
|
||||
</div>
|
||||
<button onClick={handleSelectUploadsFolder}>Browse...</button>
|
||||
<button onClick={handleSelectUploadsFolder}>{t('settings.project.browse')}...</button>
|
||||
</div>
|
||||
<div className="import-file-row">
|
||||
<label>WXR File</label>
|
||||
<label>{t('importAnalysis.wxrFile')}</label>
|
||||
<div className={`import-file-path ${!wxrFilePath ? 'placeholder' : ''}`}>
|
||||
{wxrFilePath || report?.sourceFile || 'Select a file to analyze'}
|
||||
{wxrFilePath || report?.sourceFile || t('importAnalysis.selectFileToAnalyze')}
|
||||
</div>
|
||||
<button
|
||||
className="import-analyze-btn"
|
||||
onClick={handleSelectAndAnalyze}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? 'Analyzing...' : 'Select & Analyze'}
|
||||
{isLoading ? t('importAnalysis.analyzing') : t('importAnalysis.selectAndAnalyze')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -526,7 +528,7 @@ export const ImportAnalysisView: React.FC<ImportAnalysisViewProps> = ({ definiti
|
||||
<div className="import-loading">
|
||||
<div className="import-spinner" />
|
||||
<div className="import-progress">
|
||||
<div className="import-progress-step">{progressStep || 'Analyzing WXR file...'}</div>
|
||||
<div className="import-progress-step">{progressStep || t('importAnalysis.analyzingWxr')}</div>
|
||||
{progressDetail && <div className="import-progress-detail">{progressDetail}</div>}
|
||||
</div>
|
||||
</div>
|
||||
@@ -537,7 +539,7 @@ export const ImportAnalysisView: React.FC<ImportAnalysisViewProps> = ({ definiti
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
|
||||
</svg>
|
||||
<p>Select a WordPress export file to begin analysis.</p>
|
||||
<p>{t('importAnalysis.emptyState')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -551,7 +553,7 @@ export const ImportAnalysisView: React.FC<ImportAnalysisViewProps> = ({ definiti
|
||||
{executionState.isExecuting && (
|
||||
<div className="import-execution-progress">
|
||||
<div className="import-execution-header">
|
||||
<h3>Importing...</h3>
|
||||
<h3>{t('importAnalysis.importing')}</h3>
|
||||
{executionState.eta !== null && executionState.eta > 0 && (
|
||||
<span className="import-eta">{formatEta(executionState.eta)}</span>
|
||||
)}
|
||||
@@ -576,7 +578,7 @@ export const ImportAnalysisView: React.FC<ImportAnalysisViewProps> = ({ definiti
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
|
||||
</svg>
|
||||
<span>Import completed successfully!</span>
|
||||
<span>{t('importAnalysis.importComplete')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -586,7 +588,7 @@ export const ImportAnalysisView: React.FC<ImportAnalysisViewProps> = ({ definiti
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
|
||||
</svg>
|
||||
<span>Import failed: {executionState.error}</span>
|
||||
<span>{t('importAnalysis.importFailed', { error: executionState.error })}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -598,18 +600,20 @@ export const ImportAnalysisView: React.FC<ImportAnalysisViewProps> = ({ definiti
|
||||
return (
|
||||
<div className="import-execute-section">
|
||||
<div className="import-execute-summary">
|
||||
Ready to import:
|
||||
{counts.tags > 0 && <span className="import-count-tag">{counts.tags} tags/categories</span>}
|
||||
{counts.posts > 0 && <span className="import-count-tag">{counts.posts} posts</span>}
|
||||
{counts.media > 0 && <span className="import-count-tag">{counts.media} media</span>}
|
||||
{counts.pages > 0 && <span className="import-count-tag">{counts.pages} pages</span>}
|
||||
{t('importAnalysis.readyToImport')}
|
||||
{counts.tags > 0 && <span className="import-count-tag">{counts.tags} {t('importAnalysis.tagsCategories')}</span>}
|
||||
{counts.posts > 0 && <span className="import-count-tag">{counts.posts} {t('importAnalysis.posts')}</span>}
|
||||
{counts.media > 0 && <span className="import-count-tag">{counts.media} {t('importAnalysis.media')}</span>}
|
||||
{counts.pages > 0 && <span className="import-count-tag">{counts.pages} {t('importAnalysis.pages')}</span>}
|
||||
</div>
|
||||
<button
|
||||
className="import-execute-btn"
|
||||
onClick={handleExecuteImport}
|
||||
disabled={totalImportable === 0}
|
||||
>
|
||||
{totalImportable === 0 ? 'Nothing to Import' : `Import ${totalImportable} Items`}
|
||||
{totalImportable === 0
|
||||
? t('importAnalysis.nothingToImport')
|
||||
: t('importAnalysis.importItems', { count: totalImportable })}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
@@ -618,7 +622,7 @@ export const ImportAnalysisView: React.FC<ImportAnalysisViewProps> = ({ definiti
|
||||
|
||||
{report.posts.conflicts > 0 && (
|
||||
<ConflictsSection
|
||||
title="Post Slug Conflicts"
|
||||
title={t('importAnalysis.postSlugConflicts')}
|
||||
items={report.posts.items.filter(i => i.status === 'conflict')}
|
||||
expanded={expandedSections['post-conflicts'] ?? true}
|
||||
onToggle={() => toggleSection('post-conflicts')}
|
||||
@@ -628,7 +632,7 @@ export const ImportAnalysisView: React.FC<ImportAnalysisViewProps> = ({ definiti
|
||||
|
||||
{report.pages.conflicts > 0 && (
|
||||
<ConflictsSection
|
||||
title="Page Slug Conflicts"
|
||||
title={t('importAnalysis.pageSlugConflicts')}
|
||||
items={report.pages.items.filter(i => i.status === 'conflict')}
|
||||
expanded={expandedSections['page-conflicts'] ?? true}
|
||||
onToggle={() => toggleSection('page-conflicts')}
|
||||
@@ -641,7 +645,7 @@ export const ImportAnalysisView: React.FC<ImportAnalysisViewProps> = ({ definiti
|
||||
const postsOnly = report.posts.items.filter(i => i.wxrPost.postType === 'post');
|
||||
return postsOnly.length > 0 && (
|
||||
<PostDetailSection
|
||||
title={`Posts (${postsOnly.length})`}
|
||||
title={t('importAnalysis.postsWithCount', { count: postsOnly.length })}
|
||||
items={postsOnly}
|
||||
expanded={expandedSections['posts'] ?? false}
|
||||
onToggle={() => toggleSection('posts')}
|
||||
@@ -654,7 +658,7 @@ export const ImportAnalysisView: React.FC<ImportAnalysisViewProps> = ({ definiti
|
||||
const otherPosts = report.posts.items.filter(i => i.wxrPost.postType !== 'post');
|
||||
return otherPosts.length > 0 && (
|
||||
<PostDetailSection
|
||||
title={`Other (${otherPosts.length})`}
|
||||
title={t('importAnalysis.otherWithCount', { count: otherPosts.length })}
|
||||
items={otherPosts}
|
||||
expanded={expandedSections['other'] ?? false}
|
||||
onToggle={() => toggleSection('other')}
|
||||
@@ -665,7 +669,7 @@ export const ImportAnalysisView: React.FC<ImportAnalysisViewProps> = ({ definiti
|
||||
|
||||
{report.pages.total > 0 && (
|
||||
<PostDetailSection
|
||||
title={`Pages (${report.pages.total})`}
|
||||
title={t('importAnalysis.pagesWithCount', { count: report.pages.total })}
|
||||
items={report.pages.items}
|
||||
expanded={expandedSections['pages'] ?? false}
|
||||
onToggle={() => toggleSection('pages')}
|
||||
@@ -673,7 +677,7 @@ export const ImportAnalysisView: React.FC<ImportAnalysisViewProps> = ({ definiti
|
||||
)}
|
||||
|
||||
<MediaDetailSection
|
||||
title={`Media (${report.media.total})`}
|
||||
title={t('importAnalysis.mediaWithCount', { count: report.media.total })}
|
||||
items={report.media.items}
|
||||
expanded={expandedSections['media'] ?? false}
|
||||
onToggle={() => toggleSection('media')}
|
||||
@@ -703,28 +707,31 @@ export const ImportAnalysisView: React.FC<ImportAnalysisViewProps> = ({ definiti
|
||||
);
|
||||
};
|
||||
|
||||
const SiteInfoCard: React.FC<{ site: AnalysisReport['site']; sourceFile: string }> = ({ site, sourceFile }) => (
|
||||
<div className="import-site-info">
|
||||
const SiteInfoCard: React.FC<{ site: AnalysisReport['site']; sourceFile: string }> = ({ site, sourceFile }) => {
|
||||
const { t } = useI18n();
|
||||
|
||||
return <div className="import-site-info">
|
||||
<div className="import-site-info-item">
|
||||
<span className="info-label">Site</span>
|
||||
<span className="info-value">{site.title || 'Untitled'}</span>
|
||||
<span className="info-label">{t('importAnalysis.site')}</span>
|
||||
<span className="info-value">{site.title || t('importAnalysis.untitled')}</span>
|
||||
</div>
|
||||
<div className="import-site-info-item">
|
||||
<span className="info-label">URL</span>
|
||||
<span className="info-value">{site.link || 'N/A'}</span>
|
||||
<span className="info-label">{t('importAnalysis.url')}</span>
|
||||
<span className="info-value">{site.link || t('importAnalysis.notAvailable')}</span>
|
||||
</div>
|
||||
<div className="import-site-info-item">
|
||||
<span className="info-label">Language</span>
|
||||
<span className="info-value">{site.language || 'N/A'}</span>
|
||||
<span className="info-label">{t('importAnalysis.language')}</span>
|
||||
<span className="info-value">{site.language || t('importAnalysis.notAvailable')}</span>
|
||||
</div>
|
||||
<div className="import-site-info-item">
|
||||
<span className="info-label">File</span>
|
||||
<span className="info-label">{t('importAnalysis.file')}</span>
|
||||
<span className="info-value">{sourceFile.split(/[/\\]/).pop()}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StatCards: React.FC<{ report: AnalysisReport }> = ({ report }) => {
|
||||
const { t } = useI18n();
|
||||
// Split posts by type
|
||||
const postsOnly = report.posts.items.filter(i => i.wxrPost.postType === 'post');
|
||||
const otherPosts = report.posts.items.filter(i => i.wxrPost.postType !== 'post');
|
||||
@@ -751,80 +758,80 @@ const StatCards: React.FC<{ report: AnalysisReport }> = ({ report }) => {
|
||||
return (
|
||||
<div className="import-stat-cards">
|
||||
<div className="import-stat-card">
|
||||
<h3>Posts</h3>
|
||||
<h3>{t('importAnalysis.posts')}</h3>
|
||||
<div className="import-stat-number">{postsStats.total}</div>
|
||||
<div className="import-stat-breakdown">
|
||||
{postsStats.new > 0 && <span className="import-stat-tag stat-new">{postsStats.new} new</span>}
|
||||
{postsStats.updates > 0 && <span className="import-stat-tag stat-update">{postsStats.updates} update</span>}
|
||||
{postsStats.conflicts > 0 && <span className="import-stat-tag stat-conflict">{postsStats.conflicts} conflict</span>}
|
||||
{postsStats.contentDuplicates > 0 && <span className="import-stat-tag stat-duplicate">{postsStats.contentDuplicates} duplicate</span>}
|
||||
{postsStats.new > 0 && <span className="import-stat-tag stat-new">{postsStats.new} {t('importAnalysis.new')}</span>}
|
||||
{postsStats.updates > 0 && <span className="import-stat-tag stat-update">{postsStats.updates} {t('importAnalysis.update')}</span>}
|
||||
{postsStats.conflicts > 0 && <span className="import-stat-tag stat-conflict">{postsStats.conflicts} {t('importAnalysis.conflict')}</span>}
|
||||
{postsStats.contentDuplicates > 0 && <span className="import-stat-tag stat-duplicate">{postsStats.contentDuplicates} {t('importAnalysis.duplicate')}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{otherStats.total > 0 && (
|
||||
<div className="import-stat-card">
|
||||
<h3 title={otherTypes}>Other</h3>
|
||||
<h3 title={otherTypes}>{t('importAnalysis.other')}</h3>
|
||||
<div className="import-stat-number">{otherStats.total}</div>
|
||||
<div className="import-stat-breakdown">
|
||||
{otherStats.new > 0 && <span className="import-stat-tag stat-new">{otherStats.new} new</span>}
|
||||
{otherStats.updates > 0 && <span className="import-stat-tag stat-update">{otherStats.updates} update</span>}
|
||||
{otherStats.conflicts > 0 && <span className="import-stat-tag stat-conflict">{otherStats.conflicts} conflict</span>}
|
||||
{otherStats.contentDuplicates > 0 && <span className="import-stat-tag stat-duplicate">{otherStats.contentDuplicates} duplicate</span>}
|
||||
{otherStats.new > 0 && <span className="import-stat-tag stat-new">{otherStats.new} {t('importAnalysis.new')}</span>}
|
||||
{otherStats.updates > 0 && <span className="import-stat-tag stat-update">{otherStats.updates} {t('importAnalysis.update')}</span>}
|
||||
{otherStats.conflicts > 0 && <span className="import-stat-tag stat-conflict">{otherStats.conflicts} {t('importAnalysis.conflict')}</span>}
|
||||
{otherStats.contentDuplicates > 0 && <span className="import-stat-tag stat-duplicate">{otherStats.contentDuplicates} {t('importAnalysis.duplicate')}</span>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="import-stat-card">
|
||||
<h3>Pages</h3>
|
||||
<h3>{t('importAnalysis.pages')}</h3>
|
||||
<div className="import-stat-number">{report.pages.total}</div>
|
||||
<div className="import-stat-breakdown">
|
||||
{report.pages.new > 0 && <span className="import-stat-tag stat-new">{report.pages.new} new</span>}
|
||||
{report.pages.updates > 0 && <span className="import-stat-tag stat-update">{report.pages.updates} update</span>}
|
||||
{report.pages.conflicts > 0 && <span className="import-stat-tag stat-conflict">{report.pages.conflicts} conflict</span>}
|
||||
{report.pages.contentDuplicates > 0 && <span className="import-stat-tag stat-duplicate">{report.pages.contentDuplicates} duplicate</span>}
|
||||
{report.pages.new > 0 && <span className="import-stat-tag stat-new">{report.pages.new} {t('importAnalysis.new')}</span>}
|
||||
{report.pages.updates > 0 && <span className="import-stat-tag stat-update">{report.pages.updates} {t('importAnalysis.update')}</span>}
|
||||
{report.pages.conflicts > 0 && <span className="import-stat-tag stat-conflict">{report.pages.conflicts} {t('importAnalysis.conflict')}</span>}
|
||||
{report.pages.contentDuplicates > 0 && <span className="import-stat-tag stat-duplicate">{report.pages.contentDuplicates} {t('importAnalysis.duplicate')}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="import-stat-card">
|
||||
<h3>Media</h3>
|
||||
<h3>{t('importAnalysis.media')}</h3>
|
||||
<div className="import-stat-number">{report.media.total}</div>
|
||||
<div className="import-stat-breakdown">
|
||||
{report.media.new > 0 && <span className="import-stat-tag stat-new">{report.media.new} new</span>}
|
||||
{report.media.updates > 0 && <span className="import-stat-tag stat-update">{report.media.updates} update</span>}
|
||||
{report.media.conflicts > 0 && <span className="import-stat-tag stat-conflict">{report.media.conflicts} conflict</span>}
|
||||
{report.media.contentDuplicates > 0 && <span className="import-stat-tag stat-duplicate">{report.media.contentDuplicates} duplicate</span>}
|
||||
{report.media.missing > 0 && <span className="import-stat-tag stat-missing">{report.media.missing} missing</span>}
|
||||
{report.media.new > 0 && <span className="import-stat-tag stat-new">{report.media.new} {t('importAnalysis.new')}</span>}
|
||||
{report.media.updates > 0 && <span className="import-stat-tag stat-update">{report.media.updates} {t('importAnalysis.update')}</span>}
|
||||
{report.media.conflicts > 0 && <span className="import-stat-tag stat-conflict">{report.media.conflicts} {t('importAnalysis.conflict')}</span>}
|
||||
{report.media.contentDuplicates > 0 && <span className="import-stat-tag stat-duplicate">{report.media.contentDuplicates} {t('importAnalysis.duplicate')}</span>}
|
||||
{report.media.missing > 0 && <span className="import-stat-tag stat-missing">{report.media.missing} {t('importAnalysis.missing')}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="import-stat-card">
|
||||
<h3>Categories</h3>
|
||||
<h3>{t('importAnalysis.categories')}</h3>
|
||||
<div className="import-stat-number">{report.categories.length}</div>
|
||||
<div className="import-stat-breakdown">
|
||||
{report.categories.filter(c => c.existsInProject).length > 0 && (
|
||||
<span className="import-stat-tag stat-update">{report.categories.filter(c => c.existsInProject).length} existing</span>
|
||||
<span className="import-stat-tag stat-update">{report.categories.filter(c => c.existsInProject).length} {t('importAnalysis.existing')}</span>
|
||||
)}
|
||||
{report.categories.filter(c => !c.existsInProject && c.mappedTo).length > 0 && (
|
||||
<span className="import-stat-tag stat-mapped">{report.categories.filter(c => !c.existsInProject && c.mappedTo).length} mapped</span>
|
||||
<span className="import-stat-tag stat-mapped">{report.categories.filter(c => !c.existsInProject && c.mappedTo).length} {t('importAnalysis.mapped')}</span>
|
||||
)}
|
||||
{report.categories.filter(c => !c.existsInProject && !c.mappedTo).length > 0 && (
|
||||
<span className="import-stat-tag stat-new">{report.categories.filter(c => !c.existsInProject && !c.mappedTo).length} new</span>
|
||||
<span className="import-stat-tag stat-new">{report.categories.filter(c => !c.existsInProject && !c.mappedTo).length} {t('importAnalysis.new')}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="import-stat-card">
|
||||
<h3>Tags</h3>
|
||||
<h3>{t('importAnalysis.tags')}</h3>
|
||||
<div className="import-stat-number">{report.tags.length}</div>
|
||||
<div className="import-stat-breakdown">
|
||||
{report.tags.filter(t => t.existsInProject).length > 0 && (
|
||||
<span className="import-stat-tag stat-update">{report.tags.filter(t => t.existsInProject).length} existing</span>
|
||||
<span className="import-stat-tag stat-update">{report.tags.filter(t => t.existsInProject).length} {t('importAnalysis.existing')}</span>
|
||||
)}
|
||||
{report.tags.filter(t => !t.existsInProject && t.mappedTo).length > 0 && (
|
||||
<span className="import-stat-tag stat-mapped">{report.tags.filter(t => !t.existsInProject && t.mappedTo).length} mapped</span>
|
||||
<span className="import-stat-tag stat-mapped">{report.tags.filter(t => !t.existsInProject && t.mappedTo).length} {t('importAnalysis.mapped')}</span>
|
||||
)}
|
||||
{report.tags.filter(t => !t.existsInProject && !t.mappedTo).length > 0 && (
|
||||
<span className="import-stat-tag stat-new">{report.tags.filter(t => !t.existsInProject && !t.mappedTo).length} new</span>
|
||||
<span className="import-stat-tag stat-new">{report.tags.filter(t => !t.existsInProject && !t.mappedTo).length} {t('importAnalysis.new')}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -838,6 +845,7 @@ interface DateDistribution {
|
||||
}
|
||||
|
||||
const DateDistributionCard: React.FC<{ distribution: DateDistribution }> = ({ distribution }) => {
|
||||
const { t } = useI18n();
|
||||
const postYears = Object.keys(distribution.posts).map(Number).sort();
|
||||
const mediaYears = Object.keys(distribution.media).map(Number).sort();
|
||||
const allYears = [...new Set([...postYears, ...mediaYears])].sort();
|
||||
@@ -853,12 +861,12 @@ const DateDistributionCard: React.FC<{ distribution: DateDistribution }> = ({ di
|
||||
|
||||
return (
|
||||
<div className="import-date-distribution">
|
||||
<h3>Date Distribution</h3>
|
||||
<h3>{t('importAnalysis.dateDistribution')}</h3>
|
||||
<div className="distribution-grid">
|
||||
<div className="distribution-column">
|
||||
<div className="distribution-header">
|
||||
<span className="distribution-label">Posts/Pages</span>
|
||||
<span className="distribution-total">{totalPosts} total</span>
|
||||
<span className="distribution-label">{t('importAnalysis.postsPages')}</span>
|
||||
<span className="distribution-total">{totalPosts} {t('importAnalysis.total')}</span>
|
||||
</div>
|
||||
<div className="distribution-bars">
|
||||
{allYears.map(year => {
|
||||
@@ -881,8 +889,8 @@ const DateDistributionCard: React.FC<{ distribution: DateDistribution }> = ({ di
|
||||
</div>
|
||||
<div className="distribution-column">
|
||||
<div className="distribution-header">
|
||||
<span className="distribution-label">Media</span>
|
||||
<span className="distribution-total">{totalMedia} total</span>
|
||||
<span className="distribution-label">{t('importAnalysis.media')}</span>
|
||||
<span className="distribution-total">{totalMedia} {t('importAnalysis.total')}</span>
|
||||
</div>
|
||||
<div className="distribution-bars">
|
||||
{allYears.map(year => {
|
||||
@@ -909,22 +917,22 @@ const DateDistributionCard: React.FC<{ distribution: DateDistribution }> = ({ di
|
||||
};
|
||||
|
||||
// Helper function to format post metadata for tooltip (new post from WXR)
|
||||
function formatPostTooltip(wxrPost: AnalyzedPostItem['wxrPost']): string {
|
||||
function formatPostTooltip(wxrPost: AnalyzedPostItem['wxrPost'], t: (key: string, params?: Record<string, unknown>) => string): string {
|
||||
const lines: string[] = [];
|
||||
lines.push(`WordPress ID: ${wxrPost.wpId}`);
|
||||
lines.push(`Type: ${wxrPost.postType}`);
|
||||
lines.push(`Author: ${wxrPost.creator || 'Unknown'}`);
|
||||
lines.push(`${t('importAnalysis.wordpressId')}: ${wxrPost.wpId}`);
|
||||
lines.push(`${t('importAnalysis.type')}: ${wxrPost.postType}`);
|
||||
lines.push(`${t('importAnalysis.author')}: ${wxrPost.creator || t('importAnalysis.unknown')}`);
|
||||
if (wxrPost.pubDate) {
|
||||
lines.push(`Published: ${new Date(wxrPost.pubDate).toLocaleDateString()}`);
|
||||
lines.push(`${t('importAnalysis.published')}: ${new Date(wxrPost.pubDate).toLocaleDateString()}`);
|
||||
}
|
||||
if (wxrPost.excerpt) {
|
||||
const shortExcerpt = wxrPost.excerpt.length > 100
|
||||
? wxrPost.excerpt.substring(0, 100) + '...'
|
||||
: wxrPost.excerpt;
|
||||
lines.push(`Excerpt: ${shortExcerpt}`);
|
||||
lines.push(`${t('importAnalysis.excerpt')}: ${shortExcerpt}`);
|
||||
}
|
||||
if (wxrPost.tags.length > 0) {
|
||||
lines.push(`Tags: ${wxrPost.tags.join(', ')}`);
|
||||
lines.push(`${t('importAnalysis.tags')}: ${wxrPost.tags.join(', ')}`);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
@@ -937,6 +945,7 @@ function PostHoverCard({ children, className, metadata, contentPreview, onHover
|
||||
contentPreview?: string | null;
|
||||
onHover?: () => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [pos, setPos] = useState<{ top: number; left: number }>({ top: 0, left: 0 });
|
||||
const triggerRef = useRef<HTMLSpanElement>(null);
|
||||
@@ -975,17 +984,17 @@ function PostHoverCard({ children, className, metadata, contentPreview, onHover
|
||||
<div className="post-hover-card" style={{ position: 'fixed', top: pos.top, left: pos.left }}>
|
||||
<div className="post-hover-title">{metadata.title}</div>
|
||||
<div className="post-hover-meta">
|
||||
{metadata.author && <span>Author: {metadata.author}</span>}
|
||||
{metadata.pubDate && <span>Published: {new Date(metadata.pubDate).toLocaleDateString()}</span>}
|
||||
{metadata.categories && metadata.categories.length > 0 && <span>Categories: {metadata.categories.join(', ')}</span>}
|
||||
{metadata.tags && metadata.tags.length > 0 && <span>Tags: {metadata.tags.join(', ')}</span>}
|
||||
{metadata.excerpt && <span>Excerpt: {metadata.excerpt.length > 100 ? metadata.excerpt.substring(0, 100) + '...' : metadata.excerpt}</span>}
|
||||
{metadata.author && <span>{t('importAnalysis.author')}: {metadata.author}</span>}
|
||||
{metadata.pubDate && <span>{t('importAnalysis.published')}: {new Date(metadata.pubDate).toLocaleDateString()}</span>}
|
||||
{metadata.categories && metadata.categories.length > 0 && <span>{t('importAnalysis.categories')}: {metadata.categories.join(', ')}</span>}
|
||||
{metadata.tags && metadata.tags.length > 0 && <span>{t('importAnalysis.tags')}: {metadata.tags.join(', ')}</span>}
|
||||
{metadata.excerpt && <span>{t('importAnalysis.excerpt')}: {metadata.excerpt.length > 100 ? metadata.excerpt.substring(0, 100) + '...' : metadata.excerpt}</span>}
|
||||
</div>
|
||||
{contentPreview !== undefined && (
|
||||
<div className="post-hover-content">
|
||||
<div className="post-hover-content-label">Content</div>
|
||||
<div className="post-hover-content-label">{t('importAnalysis.content')}</div>
|
||||
<div className="post-hover-content-text">
|
||||
{contentPreview ? (contentPreview.substring(0, 200) + (contentPreview.length > 200 ? '...' : '')) : 'Loading...'}
|
||||
{contentPreview ? (contentPreview.substring(0, 200) + (contentPreview.length > 200 ? '...' : '')) : t('importAnalysis.loading')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -1001,6 +1010,7 @@ function ExistingPostHoverCard({ children, className, postId }: {
|
||||
className?: string;
|
||||
postId: string;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const [postData, setPostData] = useState<{
|
||||
title: string; content: string; author?: string; pubDate?: string;
|
||||
tags: string[]; categories: string[]; excerpt?: string;
|
||||
@@ -1029,7 +1039,7 @@ function ExistingPostHoverCard({ children, className, postId }: {
|
||||
return (
|
||||
<PostHoverCard
|
||||
className={className}
|
||||
metadata={postData || { title: 'Loading...' }}
|
||||
metadata={postData || { title: t('importAnalysis.loading') }}
|
||||
contentPreview={loaded ? (postData?.content || null) : undefined}
|
||||
onHover={handleHover}
|
||||
>
|
||||
@@ -1039,22 +1049,22 @@ function ExistingPostHoverCard({ children, className, postId }: {
|
||||
}
|
||||
|
||||
// Helper function to format media metadata for tooltip
|
||||
function formatMediaTooltip(wxrMedia: AnalyzedMediaItem['wxrMedia']): string {
|
||||
function formatMediaTooltip(wxrMedia: AnalyzedMediaItem['wxrMedia'], t: (key: string, params?: Record<string, unknown>) => string): string {
|
||||
const lines: string[] = [];
|
||||
lines.push(`WordPress ID: ${wxrMedia.wpId}`);
|
||||
lines.push(`MIME Type: ${wxrMedia.mimeType || 'Unknown'}`);
|
||||
lines.push(`${t('importAnalysis.wordpressId')}: ${wxrMedia.wpId}`);
|
||||
lines.push(`${t('importAnalysis.mimeType')}: ${wxrMedia.mimeType || t('importAnalysis.unknown')}`);
|
||||
if (wxrMedia.pubDate) {
|
||||
lines.push(`Uploaded: ${new Date(wxrMedia.pubDate).toLocaleDateString()}`);
|
||||
lines.push(`${t('importAnalysis.uploaded')}: ${new Date(wxrMedia.pubDate).toLocaleDateString()}`);
|
||||
}
|
||||
if (wxrMedia.parentId) {
|
||||
lines.push(`Parent Post ID: ${wxrMedia.parentId}`);
|
||||
lines.push(`${t('importAnalysis.parentPostId')}: ${wxrMedia.parentId}`);
|
||||
}
|
||||
lines.push(`URL: ${wxrMedia.url}`);
|
||||
lines.push(`${t('importAnalysis.url')}: ${wxrMedia.url}`);
|
||||
if (wxrMedia.description) {
|
||||
const shortDesc = wxrMedia.description.length > 100
|
||||
? wxrMedia.description.substring(0, 100) + '...'
|
||||
: wxrMedia.description;
|
||||
lines.push(`Description: ${shortDesc}`);
|
||||
lines.push(`${t('importAnalysis.description')}: ${shortDesc}`);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
@@ -1065,70 +1075,74 @@ const ConflictsSection: React.FC<{
|
||||
expanded: boolean;
|
||||
onToggle: () => void;
|
||||
onResolutionChange: (slug: string, resolution: ImportConflictResolution) => void;
|
||||
}> = ({ title, items, expanded, onToggle, onResolutionChange }) => (
|
||||
<div className="import-detail-section conflicts-section">
|
||||
<h3 onClick={onToggle}>
|
||||
<span className={`toggle-icon ${expanded ? 'open' : ''}`}>▶</span>
|
||||
{title} ({items.length})
|
||||
</h3>
|
||||
{expanded && (
|
||||
<table className="import-detail-table conflicts-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Slug</th>
|
||||
<th>New Entry (WXR)</th>
|
||||
<th>Existing Entry</th>
|
||||
<th>Resolution</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item, idx) => (
|
||||
<tr key={idx} className="conflict-row">
|
||||
<td className="slug-cell">{item.wxrPost.slug}</td>
|
||||
<td className="new-entry-cell">
|
||||
<PostHoverCard
|
||||
className="entry-title tooltip-target"
|
||||
metadata={{ title: item.wxrPost.title, author: item.wxrPost.creator, pubDate: item.wxrPost.pubDate, categories: item.wxrPost.categories, tags: item.wxrPost.tags }}
|
||||
contentPreview={item.markdownPreview}
|
||||
>
|
||||
{item.wxrPost.title}
|
||||
</PostHoverCard>
|
||||
{item.wxrPost.categories.length > 0 && (
|
||||
<span className="entry-categories">
|
||||
{item.wxrPost.categories.join(', ')}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="existing-entry-cell">
|
||||
{item.existingPost ? (
|
||||
<ExistingPostHoverCard
|
||||
className="entry-title tooltip-target"
|
||||
postId={item.existingPost.id}
|
||||
>
|
||||
{item.existingPost.title}
|
||||
</ExistingPostHoverCard>
|
||||
) : (
|
||||
<span className="entry-title">--</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="resolution-cell">
|
||||
<select
|
||||
className="resolution-select"
|
||||
value={item.conflictResolution || 'ignore'}
|
||||
onChange={(e) => onResolutionChange(item.wxrPost.slug, e.target.value as ImportConflictResolution)}
|
||||
>
|
||||
<option value="ignore">Ignore</option>
|
||||
<option value="overwrite">Overwrite</option>
|
||||
<option value="import">Import (new slug)</option>
|
||||
</select>
|
||||
</td>
|
||||
}> = ({ title, items, expanded, onToggle, onResolutionChange }) => {
|
||||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
<div className="import-detail-section conflicts-section">
|
||||
<h3 onClick={onToggle}>
|
||||
<span className={`toggle-icon ${expanded ? 'open' : ''}`}>▶</span>
|
||||
{title} ({items.length})
|
||||
</h3>
|
||||
{expanded && (
|
||||
<table className="import-detail-table conflicts-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('importAnalysis.slug')}</th>
|
||||
<th>{t('importAnalysis.newEntryWxr')}</th>
|
||||
<th>{t('importAnalysis.existingEntry')}</th>
|
||||
<th>{t('importAnalysis.resolution')}</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item, idx) => (
|
||||
<tr key={idx} className="conflict-row">
|
||||
<td className="slug-cell">{item.wxrPost.slug}</td>
|
||||
<td className="new-entry-cell">
|
||||
<PostHoverCard
|
||||
className="entry-title tooltip-target"
|
||||
metadata={{ title: item.wxrPost.title, author: item.wxrPost.creator, pubDate: item.wxrPost.pubDate, categories: item.wxrPost.categories, tags: item.wxrPost.tags }}
|
||||
contentPreview={item.markdownPreview}
|
||||
>
|
||||
{item.wxrPost.title}
|
||||
</PostHoverCard>
|
||||
{item.wxrPost.categories.length > 0 && (
|
||||
<span className="entry-categories">
|
||||
{item.wxrPost.categories.join(', ')}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="existing-entry-cell">
|
||||
{item.existingPost ? (
|
||||
<ExistingPostHoverCard
|
||||
className="entry-title tooltip-target"
|
||||
postId={item.existingPost.id}
|
||||
>
|
||||
{item.existingPost.title}
|
||||
</ExistingPostHoverCard>
|
||||
) : (
|
||||
<span className="entry-title">{t('importAnalysis.none')}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="resolution-cell">
|
||||
<select
|
||||
className="resolution-select"
|
||||
value={item.conflictResolution || 'ignore'}
|
||||
onChange={(e) => onResolutionChange(item.wxrPost.slug, e.target.value as ImportConflictResolution)}
|
||||
>
|
||||
<option value="ignore">{t('importAnalysis.ignore')}</option>
|
||||
<option value="overwrite">{t('importAnalysis.overwrite')}</option>
|
||||
<option value="import">{t('importAnalysis.importNewSlug')}</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PostDetailSection: React.FC<{
|
||||
title: string;
|
||||
@@ -1136,84 +1150,92 @@ const PostDetailSection: React.FC<{
|
||||
expanded: boolean;
|
||||
onToggle: () => void;
|
||||
showType?: boolean;
|
||||
}> = ({ title, items, expanded, onToggle, showType }) => (
|
||||
<div className="import-detail-section">
|
||||
<h3 onClick={onToggle}>
|
||||
<span className={`toggle-icon ${expanded ? 'open' : ''}`}>▶</span>
|
||||
{title}
|
||||
</h3>
|
||||
{expanded && (
|
||||
<table className="import-detail-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
{showType && <th>Type</th>}
|
||||
<th>Title</th>
|
||||
<th>Slug</th>
|
||||
<th>Categories</th>
|
||||
<th>WP Status</th>
|
||||
<th>Existing Match</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item, idx) => (
|
||||
<tr key={idx} className="post-row-with-tooltip" title={formatPostTooltip(item.wxrPost)}>
|
||||
<td><span className={`status-badge ${item.status}`}>{item.status}</span></td>
|
||||
{showType && <td className="post-type-cell">{item.wxrPost.postType}</td>}
|
||||
<td>{item.wxrPost.title}</td>
|
||||
<td className="slug-cell">{item.wxrPost.slug}</td>
|
||||
<td className="categories-cell">
|
||||
{item.wxrPost.categories.length > 0
|
||||
? item.wxrPost.categories.join(', ')
|
||||
: '--'}
|
||||
</td>
|
||||
<td>{item.wxrPost.status}</td>
|
||||
<td className="existing-match">{item.existingPost?.title || '--'}</td>
|
||||
}> = ({ title, items, expanded, onToggle, showType }) => {
|
||||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
<div className="import-detail-section">
|
||||
<h3 onClick={onToggle}>
|
||||
<span className={`toggle-icon ${expanded ? 'open' : ''}`}>▶</span>
|
||||
{title}
|
||||
</h3>
|
||||
{expanded && (
|
||||
<table className="import-detail-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('importAnalysis.status')}</th>
|
||||
{showType && <th>{t('importAnalysis.type')}</th>}
|
||||
<th>{t('importAnalysis.title')}</th>
|
||||
<th>{t('importAnalysis.slug')}</th>
|
||||
<th>{t('importAnalysis.categories')}</th>
|
||||
<th>{t('importAnalysis.wpStatus')}</th>
|
||||
<th>{t('importAnalysis.existingMatch')}</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item, idx) => (
|
||||
<tr key={idx} className="post-row-with-tooltip" title={formatPostTooltip(item.wxrPost, t)}>
|
||||
<td><span className={`status-badge ${item.status}`}>{item.status}</span></td>
|
||||
{showType && <td className="post-type-cell">{item.wxrPost.postType}</td>}
|
||||
<td>{item.wxrPost.title}</td>
|
||||
<td className="slug-cell">{item.wxrPost.slug}</td>
|
||||
<td className="categories-cell">
|
||||
{item.wxrPost.categories.length > 0
|
||||
? item.wxrPost.categories.join(', ')
|
||||
: t('importAnalysis.none')}
|
||||
</td>
|
||||
<td>{item.wxrPost.status}</td>
|
||||
<td className="existing-match">{item.existingPost?.title || t('importAnalysis.none')}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MediaDetailSection: React.FC<{
|
||||
title: string;
|
||||
items: AnalyzedMediaItem[];
|
||||
expanded: boolean;
|
||||
onToggle: () => void;
|
||||
}> = ({ title, items, expanded, onToggle }) => (
|
||||
<div className="import-detail-section">
|
||||
<h3 onClick={onToggle}>
|
||||
<span className={`toggle-icon ${expanded ? 'open' : ''}`}>▶</span>
|
||||
{title}
|
||||
</h3>
|
||||
{expanded && (
|
||||
<table className="import-detail-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>Filename</th>
|
||||
<th>Type</th>
|
||||
<th>Path</th>
|
||||
<th>Existing Match</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item, idx) => (
|
||||
<tr key={idx} className="media-row-with-tooltip" title={formatMediaTooltip(item.wxrMedia)}>
|
||||
<td><span className={`status-badge ${item.status}`}>{item.status}</span></td>
|
||||
<td>{item.wxrMedia.filename}</td>
|
||||
<td className="mime-type-cell">{item.wxrMedia.mimeType || '--'}</td>
|
||||
<td className="slug-cell">{item.wxrMedia.relativePath}</td>
|
||||
<td className="existing-match">{item.existingMedia?.originalName || '--'}</td>
|
||||
}> = ({ title, items, expanded, onToggle }) => {
|
||||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
<div className="import-detail-section">
|
||||
<h3 onClick={onToggle}>
|
||||
<span className={`toggle-icon ${expanded ? 'open' : ''}`}>▶</span>
|
||||
{title}
|
||||
</h3>
|
||||
{expanded && (
|
||||
<table className="import-detail-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('importAnalysis.status')}</th>
|
||||
<th>{t('importAnalysis.filename')}</th>
|
||||
<th>{t('importAnalysis.type')}</th>
|
||||
<th>{t('importAnalysis.path')}</th>
|
||||
<th>{t('importAnalysis.existingMatch')}</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item, idx) => (
|
||||
<tr key={idx} className="media-row-with-tooltip" title={formatMediaTooltip(item.wxrMedia, t)}>
|
||||
<td><span className={`status-badge ${item.status}`}>{item.status}</span></td>
|
||||
<td>{item.wxrMedia.filename}</td>
|
||||
<td className="mime-type-cell">{item.wxrMedia.mimeType || t('importAnalysis.none')}</td>
|
||||
<td className="slug-cell">{item.wxrMedia.relativePath}</td>
|
||||
<td className="existing-match">{item.existingMedia?.originalName || t('importAnalysis.none')}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TaxonomySection: React.FC<{
|
||||
categories: TaxonomyItem[];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useI18n } from '../../i18n';
|
||||
import './PostSearchModal.css';
|
||||
|
||||
interface SearchResult {
|
||||
@@ -19,6 +20,7 @@ export const PostSearchModal: React.FC<PostSearchModalProps> = ({
|
||||
onClose,
|
||||
initialQuery = ''
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const [query, setQuery] = useState(initialQuery);
|
||||
const [results, setResults] = useState<SearchResult[]>([]);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
@@ -107,7 +109,7 @@ export const PostSearchModal: React.FC<PostSearchModalProps> = ({
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
className="post-search-input"
|
||||
placeholder="Search posts by title or content..."
|
||||
placeholder={t('postSearch.placeholder')}
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
autoComplete="off"
|
||||
@@ -117,19 +119,19 @@ export const PostSearchModal: React.FC<PostSearchModalProps> = ({
|
||||
<div className="post-search-results">
|
||||
{isSearching && (
|
||||
<div className="post-search-loading">
|
||||
Searching...
|
||||
{t('postSearch.searching')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isSearching && query.length < 2 && (
|
||||
<div className="post-search-empty">
|
||||
Type at least 2 characters to search
|
||||
{t('postSearch.typeMore')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isSearching && query.length >= 2 && results.length === 0 && (
|
||||
<div className="post-search-empty">
|
||||
No posts found for "{query}"
|
||||
{t('postSearch.noResults', { query })}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -155,7 +157,7 @@ export const PostSearchModal: React.FC<PostSearchModalProps> = ({
|
||||
|
||||
<div className="post-search-footer">
|
||||
<span className="post-search-hint">
|
||||
Use ↑↓ to navigate, Enter to select, Esc to close
|
||||
{t('postSearch.hint')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -484,13 +484,13 @@ export const SettingsView: React.FC = () => {
|
||||
<SettingSection
|
||||
id="settings-section-editor"
|
||||
title={t('settings.editor.title')}
|
||||
description="Configure the blog post editor behavior and appearance."
|
||||
description={t('settings.editor.description')}
|
||||
hidden={!sectionHasMatches(editorKeywords)}
|
||||
>
|
||||
<SettingRow
|
||||
id="editor-mode"
|
||||
label="Default Editor Mode"
|
||||
description="Choose the default mode when opening posts. You can switch modes at any time using the editor toolbar."
|
||||
label={t('settings.editor.defaultModeLabel')}
|
||||
description={t('settings.editor.defaultModeDescription')}
|
||||
>
|
||||
<select
|
||||
id="editor-mode"
|
||||
@@ -505,12 +505,12 @@ export const SettingsView: React.FC = () => {
|
||||
|
||||
<SettingRow
|
||||
id="diff-view-style"
|
||||
label="Diff View Style"
|
||||
description="Choose how Git diffs are shown by default."
|
||||
label={t('settings.editor.diffViewStyleLabel')}
|
||||
description={t('settings.editor.diffViewStyleDescription')}
|
||||
>
|
||||
<select
|
||||
id="diff-view-style"
|
||||
aria-label="Diff View Style"
|
||||
aria-label={t('settings.editor.diffViewStyleLabel')}
|
||||
value={gitDiffPreferences.viewStyle}
|
||||
onChange={(e) =>
|
||||
setGitDiffPreferences({
|
||||
@@ -526,12 +526,12 @@ export const SettingsView: React.FC = () => {
|
||||
|
||||
<SettingRow
|
||||
id="diff-wrap-long-lines"
|
||||
label="Wrap Long Lines in Diff"
|
||||
description="Enable word wrapping for long lines in Git diffs."
|
||||
label={t('settings.editor.wrapLongLinesLabel')}
|
||||
description={t('settings.editor.wrapLongLinesDescription')}
|
||||
>
|
||||
<input
|
||||
id="diff-wrap-long-lines"
|
||||
aria-label="Wrap long lines in diff"
|
||||
aria-label={t('settings.editor.wrapLongLinesAria')}
|
||||
type="checkbox"
|
||||
checked={gitDiffPreferences.wordWrap}
|
||||
onChange={(e) =>
|
||||
@@ -545,12 +545,12 @@ export const SettingsView: React.FC = () => {
|
||||
|
||||
<SettingRow
|
||||
id="diff-hide-unchanged-regions"
|
||||
label="Hide Unchanged Regions"
|
||||
description="Collapse unchanged regions in Git diffs."
|
||||
label={t('settings.editor.hideUnchangedRegionsLabel')}
|
||||
description={t('settings.editor.hideUnchangedRegionsDescription')}
|
||||
>
|
||||
<input
|
||||
id="diff-hide-unchanged-regions"
|
||||
aria-label="Hide unchanged regions"
|
||||
aria-label={t('settings.editor.hideUnchangedRegionsAria')}
|
||||
type="checkbox"
|
||||
checked={gitDiffPreferences.hideUnchangedRegions}
|
||||
onChange={(e) =>
|
||||
@@ -717,7 +717,7 @@ export const SettingsView: React.FC = () => {
|
||||
<div className="category-add-form">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="New category name..."
|
||||
placeholder={t('settings.content.newCategoryPlaceholder')}
|
||||
value={newCategoryInput}
|
||||
onChange={(e) => setNewCategoryInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
@@ -728,13 +728,13 @@ export const SettingsView: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
<button className="primary" onClick={handleAddCategory}>
|
||||
Add Category
|
||||
{t('settings.content.addCategory')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="setting-actions">
|
||||
<button className="secondary" onClick={handleResetCategories}>
|
||||
Reset to Defaults
|
||||
{t('settings.content.resetDefaults')}
|
||||
</button>
|
||||
</div>
|
||||
</SettingSection>
|
||||
@@ -813,8 +813,8 @@ export const SettingsView: React.FC = () => {
|
||||
>
|
||||
<SettingRow
|
||||
id="ai-api-key"
|
||||
label="OpenCode API Key"
|
||||
description="Your API key for the OpenCode Zen gateway. Required to use AI features."
|
||||
label={t('settings.ai.apiKeyLabel')}
|
||||
description={t('settings.ai.apiKeyDescription')}
|
||||
>
|
||||
<div className="setting-input-group">
|
||||
{aiHasApiKey ? (
|
||||
@@ -824,9 +824,9 @@ export const SettingsView: React.FC = () => {
|
||||
type="text"
|
||||
value={aiApiKeyMasked}
|
||||
disabled
|
||||
placeholder="API key configured"
|
||||
placeholder={t('settings.ai.apiKeyConfigured')}
|
||||
/>
|
||||
<span className="setting-status-badge success">✓ Configured</span>
|
||||
<span className="setting-status-badge success">{t('settings.ai.configured')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -835,10 +835,10 @@ export const SettingsView: React.FC = () => {
|
||||
type="password"
|
||||
value={newApiKey}
|
||||
onChange={(e) => setNewApiKey(e.target.value)}
|
||||
placeholder="Enter your API key..."
|
||||
placeholder={t('chat.apiKeyPlaceholder')}
|
||||
/>
|
||||
<button className="primary" onClick={handleSaveApiKey} disabled={!newApiKey.trim()}>
|
||||
Save Key
|
||||
{t('chat.apiKeySave')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
@@ -846,7 +846,7 @@ export const SettingsView: React.FC = () => {
|
||||
{aiHasApiKey && (
|
||||
<div className="setting-inline-action">
|
||||
<button className="text-button" onClick={() => { setAiHasApiKey(false); setAiApiKeyMasked(''); }}>
|
||||
Change API Key
|
||||
{t('settings.ai.changeApiKey')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -854,8 +854,8 @@ export const SettingsView: React.FC = () => {
|
||||
|
||||
<SettingRow
|
||||
id="ai-model"
|
||||
label="Default Model"
|
||||
description="The AI model to use for new chat conversations."
|
||||
label={t('settings.ai.defaultModelLabel')}
|
||||
description={t('settings.ai.defaultModelDescription')}
|
||||
>
|
||||
<select
|
||||
id="ai-model"
|
||||
@@ -872,8 +872,8 @@ export const SettingsView: React.FC = () => {
|
||||
|
||||
<SettingRow
|
||||
id="ai-system-prompt"
|
||||
label="System Prompt"
|
||||
description="Instructions given to the AI at the start of each conversation. This defines how the assistant behaves and what tools it knows about."
|
||||
label={t('settings.ai.systemPromptLabel')}
|
||||
description={t('settings.ai.systemPromptDescription')}
|
||||
>
|
||||
<textarea
|
||||
id="ai-system-prompt"
|
||||
@@ -882,7 +882,7 @@ export const SettingsView: React.FC = () => {
|
||||
setAiSystemPrompt(e.target.value);
|
||||
setAiSystemPromptModified(true);
|
||||
}}
|
||||
placeholder="Enter system instructions for the AI assistant..."
|
||||
placeholder={t('settings.ai.systemPromptPlaceholder')}
|
||||
rows={12}
|
||||
className="system-prompt-textarea"
|
||||
/>
|
||||
@@ -892,10 +892,10 @@ export const SettingsView: React.FC = () => {
|
||||
onClick={handleSaveSystemPrompt}
|
||||
disabled={!aiSystemPromptModified}
|
||||
>
|
||||
Save Prompt
|
||||
{t('settings.ai.savePrompt')}
|
||||
</button>
|
||||
<button className="secondary" onClick={handleResetSystemPrompt}>
|
||||
Reset to Default
|
||||
{t('settings.ai.resetPrompt')}
|
||||
</button>
|
||||
</div>
|
||||
</SettingRow>
|
||||
@@ -907,18 +907,18 @@ export const SettingsView: React.FC = () => {
|
||||
<SettingSection
|
||||
id="settings-section-publishing"
|
||||
title={t('settings.publishing.ftpTitle')}
|
||||
description="Configure FTP credentials for publishing your blog to a web server."
|
||||
description={t('credentials.ftp.description')}
|
||||
hidden={!sectionHasMatches(publishingKeywords)}
|
||||
>
|
||||
<SettingRow
|
||||
id="ftp-host"
|
||||
label="Host"
|
||||
description="The FTP server hostname or IP address."
|
||||
label={t('credentials.field.host')}
|
||||
description={t('settings.publishing.ftpHostDescription')}
|
||||
>
|
||||
<input
|
||||
id="ftp-host"
|
||||
type="text"
|
||||
placeholder="ftp.example.com"
|
||||
placeholder={t('credentials.ftp.placeholder.host')}
|
||||
value={credentials.ftpHost}
|
||||
onChange={(e) => setCredentials({ ...credentials, ftpHost: e.target.value })}
|
||||
/>
|
||||
@@ -926,13 +926,13 @@ export const SettingsView: React.FC = () => {
|
||||
|
||||
<SettingRow
|
||||
id="ftp-user"
|
||||
label="Username"
|
||||
description="Your FTP account username."
|
||||
label={t('credentials.field.username')}
|
||||
description={t('settings.publishing.ftpUsernameDescription')}
|
||||
>
|
||||
<input
|
||||
id="ftp-user"
|
||||
type="text"
|
||||
placeholder="ftp-user"
|
||||
placeholder={t('credentials.ftp.placeholder.username')}
|
||||
value={credentials.ftpUser}
|
||||
onChange={(e) => setCredentials({ ...credentials, ftpUser: e.target.value })}
|
||||
/>
|
||||
@@ -940,21 +940,21 @@ export const SettingsView: React.FC = () => {
|
||||
|
||||
<SettingRow
|
||||
id="ftp-password"
|
||||
label="Password"
|
||||
description="Your FTP account password."
|
||||
label={t('credentials.field.password')}
|
||||
description={t('settings.publishing.ftpPasswordDescription')}
|
||||
>
|
||||
<div className="setting-input-group">
|
||||
<input
|
||||
id="ftp-password"
|
||||
type={showSecrets ? 'text' : 'password'}
|
||||
placeholder="Password"
|
||||
placeholder={t('credentials.ftp.placeholder.password')}
|
||||
value={credentials.ftpPassword}
|
||||
onChange={(e) => setCredentials({ ...credentials, ftpPassword: e.target.value })}
|
||||
/>
|
||||
<button
|
||||
className="setting-toggle-visibility"
|
||||
onClick={() => setShowSecrets(!showSecrets)}
|
||||
title={showSecrets ? 'Hide password' : 'Show password'}
|
||||
title={showSecrets ? t('settings.publishing.hidePassword') : t('settings.publishing.showPassword')}
|
||||
>
|
||||
{showSecrets ? '🔒' : '👁'}
|
||||
</button>
|
||||
@@ -969,18 +969,18 @@ export const SettingsView: React.FC = () => {
|
||||
|
||||
<SettingSection
|
||||
title={t('settings.publishing.sshTitle')}
|
||||
description="Configure SSH credentials for secure deployment to your server."
|
||||
description={t('credentials.ssh.description')}
|
||||
hidden={!sectionHasMatches(publishingKeywords)}
|
||||
>
|
||||
<SettingRow
|
||||
id="ssh-host"
|
||||
label="Host"
|
||||
description="The SSH server hostname or IP address."
|
||||
label={t('credentials.field.host')}
|
||||
description={t('settings.publishing.sshHostDescription')}
|
||||
>
|
||||
<input
|
||||
id="ssh-host"
|
||||
type="text"
|
||||
placeholder="server.example.com"
|
||||
placeholder={t('credentials.ssh.placeholder.host')}
|
||||
value={credentials.sshHost}
|
||||
onChange={(e) => setCredentials({ ...credentials, sshHost: e.target.value })}
|
||||
/>
|
||||
@@ -988,13 +988,13 @@ export const SettingsView: React.FC = () => {
|
||||
|
||||
<SettingRow
|
||||
id="ssh-user"
|
||||
label="Username"
|
||||
description="Your SSH account username."
|
||||
label={t('credentials.field.username')}
|
||||
description={t('settings.publishing.sshUsernameDescription')}
|
||||
>
|
||||
<input
|
||||
id="ssh-user"
|
||||
type="text"
|
||||
placeholder="ssh-user"
|
||||
placeholder={t('credentials.ssh.placeholder.username')}
|
||||
value={credentials.sshUser}
|
||||
onChange={(e) => setCredentials({ ...credentials, sshUser: e.target.value })}
|
||||
/>
|
||||
@@ -1002,13 +1002,13 @@ export const SettingsView: React.FC = () => {
|
||||
|
||||
<SettingRow
|
||||
id="ssh-keypath"
|
||||
label="SSH Key Path"
|
||||
description="Path to your SSH private key file."
|
||||
label={t('credentials.field.sshKeyPath')}
|
||||
description={t('settings.publishing.sshKeyPathDescription')}
|
||||
>
|
||||
<input
|
||||
id="ssh-keypath"
|
||||
type="text"
|
||||
placeholder="~/.ssh/id_rsa"
|
||||
placeholder={t('credentials.ssh.placeholder.keyPath')}
|
||||
value={credentials.sshKeyPath}
|
||||
onChange={(e) => setCredentials({ ...credentials, sshKeyPath: e.target.value })}
|
||||
/>
|
||||
@@ -1027,13 +1027,13 @@ export const SettingsView: React.FC = () => {
|
||||
<SettingSection
|
||||
id="settings-section-data"
|
||||
title={t('settings.data.title')}
|
||||
description="Rebuild the local database index from source files. Useful if post or media files were edited externally."
|
||||
description={t('settings.data.description')}
|
||||
hidden={!sectionHasMatches(dataKeywords)}
|
||||
>
|
||||
<SettingRow
|
||||
id="rebuild-posts"
|
||||
label="Rebuild Posts Database"
|
||||
description="Re-scan all post markdown files and rebuild the database index."
|
||||
label={t('settings.data.rebuildPostsLabel')}
|
||||
description={t('settings.data.rebuildPostsDescription')}
|
||||
>
|
||||
<button
|
||||
className="secondary"
|
||||
@@ -1053,7 +1053,7 @@ export const SettingsView: React.FC = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
Rebuild Posts
|
||||
{t('settings.data.rebuildPostsAction')}
|
||||
</button>
|
||||
</SettingRow>
|
||||
|
||||
|
||||
@@ -1355,6 +1355,7 @@ const SettingsNav: React.FC = () => {
|
||||
|
||||
// Chat conversations list
|
||||
const ChatList: React.FC = () => {
|
||||
const { t, language } = useI18n();
|
||||
const { openTab, closeTab } = useAppStore();
|
||||
const [conversations, setConversations] = useState<ChatConversation[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -1412,7 +1413,7 @@ const ChatList: React.FC = () => {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to create conversation:', error);
|
||||
showToast.error('Failed to create new chat');
|
||||
showToast.error(t('sidebar.chat.createFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1428,7 +1429,7 @@ const ChatList: React.FC = () => {
|
||||
closeTab(conversationId);
|
||||
} catch (error) {
|
||||
console.error('Failed to delete conversation:', error);
|
||||
showToast.error('Failed to delete chat');
|
||||
showToast.error(t('sidebar.chat.deleteFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1436,24 +1437,25 @@ const ChatList: React.FC = () => {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
|
||||
const uiDateLocale = UI_DATE_LOCALE[language] || UI_DATE_LOCALE.en;
|
||||
|
||||
if (diffDays === 0) {
|
||||
return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
|
||||
return date.toLocaleTimeString(uiDateLocale, { hour: 'numeric', minute: '2-digit' });
|
||||
} else if (diffDays === 1) {
|
||||
return 'Yesterday';
|
||||
return t('sidebar.chat.yesterday');
|
||||
} else if (diffDays < 7) {
|
||||
return date.toLocaleDateString('en-US', { weekday: 'short' });
|
||||
return date.toLocaleDateString(uiDateLocale, { weekday: 'short' });
|
||||
}
|
||||
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
||||
return date.toLocaleDateString(uiDateLocale, { month: 'short', day: 'numeric' });
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="chat-list">
|
||||
<div className="chat-list-header">
|
||||
<span>AI ASSISTANT</span>
|
||||
<span>{t('sidebar.chat.header')}</span>
|
||||
</div>
|
||||
<div className="chat-loading">Loading...</div>
|
||||
<div className="chat-loading">{t('sidebar.loading')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1461,22 +1463,22 @@ const ChatList: React.FC = () => {
|
||||
return (
|
||||
<div className="chat-list">
|
||||
<div className="chat-list-header">
|
||||
<span>AI ASSISTANT</span>
|
||||
<button className="chat-new-button" onClick={handleNewChat} title="New Chat">
|
||||
<span>{t('sidebar.chat.header')}</span>
|
||||
<button className="chat-new-button" onClick={handleNewChat} title={t('sidebar.chat.newChat')}>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
{!isReady && (
|
||||
<div className="chat-auth-prompt">
|
||||
<p>API key needed. Open a chat to configure.</p>
|
||||
<p>{t('sidebar.chat.apiKeyNeeded')}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="chat-list-items">
|
||||
{conversations.length === 0 ? (
|
||||
<div className="chat-empty">
|
||||
<p>No conversations yet</p>
|
||||
<p>{t('sidebar.chat.noConversations')}</p>
|
||||
<button className="chat-start-button" onClick={handleNewChat}>
|
||||
Start a new chat
|
||||
{t('sidebar.chat.startNew')}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -1496,7 +1498,7 @@ const ChatList: React.FC = () => {
|
||||
e.stopPropagation();
|
||||
handleDeleteChat(conv.id);
|
||||
}}
|
||||
title="Delete conversation"
|
||||
title={t('sidebar.chat.deleteConversation')}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
@@ -1509,6 +1511,7 @@ const ChatList: React.FC = () => {
|
||||
};
|
||||
|
||||
const ImportList: React.FC = () => {
|
||||
const { t, language } = useI18n();
|
||||
const { openTab, closeTab, activeProject } = useAppStore();
|
||||
const [definitions, setDefinitions] = useState<ImportDefinitionData[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -1560,7 +1563,7 @@ const ImportList: React.FC = () => {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to create import definition:', error);
|
||||
showToast.error('Failed to create import definition');
|
||||
showToast.error(t('sidebar.import.createFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1576,7 +1579,7 @@ const ImportList: React.FC = () => {
|
||||
closeTab(definitionId);
|
||||
} catch (error) {
|
||||
console.error('Failed to delete import definition:', error);
|
||||
showToast.error('Failed to delete import definition');
|
||||
showToast.error(t('sidebar.import.deleteFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1584,23 +1587,24 @@ const ImportList: React.FC = () => {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
|
||||
const uiDateLocale = UI_DATE_LOCALE[language] || UI_DATE_LOCALE.en;
|
||||
if (diffDays === 0) {
|
||||
return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
|
||||
return date.toLocaleTimeString(uiDateLocale, { hour: 'numeric', minute: '2-digit' });
|
||||
} else if (diffDays === 1) {
|
||||
return 'Yesterday';
|
||||
return t('sidebar.chat.yesterday');
|
||||
} else if (diffDays < 7) {
|
||||
return date.toLocaleDateString('en-US', { weekday: 'short' });
|
||||
return date.toLocaleDateString(uiDateLocale, { weekday: 'short' });
|
||||
}
|
||||
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
||||
return date.toLocaleDateString(uiDateLocale, { month: 'short', day: 'numeric' });
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="chat-list">
|
||||
<div className="chat-list-header">
|
||||
<span>IMPORTS</span>
|
||||
<span>{t('sidebar.import.header')}</span>
|
||||
</div>
|
||||
<div className="chat-loading">Loading...</div>
|
||||
<div className="chat-loading">{t('sidebar.loading')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1608,17 +1612,17 @@ const ImportList: React.FC = () => {
|
||||
return (
|
||||
<div className="chat-list">
|
||||
<div className="chat-list-header">
|
||||
<span>IMPORTS</span>
|
||||
<button className="chat-new-button" onClick={handleNewDefinition} title="New Import Definition">
|
||||
<span>{t('sidebar.import.header')}</span>
|
||||
<button className="chat-new-button" onClick={handleNewDefinition} title={t('sidebar.import.newDefinition')}>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<div className="chat-list-items">
|
||||
{definitions.length === 0 ? (
|
||||
<div className="chat-empty">
|
||||
<p>No import definitions yet</p>
|
||||
<p>{t('sidebar.import.none')}</p>
|
||||
<button className="chat-start-button" onClick={handleNewDefinition}>
|
||||
Create an import definition
|
||||
{t('sidebar.import.createDefinition')}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -1635,7 +1639,7 @@ const ImportList: React.FC = () => {
|
||||
<button
|
||||
className="chat-item-delete"
|
||||
onClick={(e) => handleDeleteDefinition(e, def.id)}
|
||||
title="Delete import definition"
|
||||
title={t('sidebar.import.deleteDefinition')}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
|
||||
@@ -68,22 +68,22 @@ export const StatusBar: React.FC = () => {
|
||||
|
||||
{/* Stats */}
|
||||
<div className="status-bar-item">
|
||||
<span>{totalPosts} posts</span>
|
||||
<span>{t('statusBar.posts', { count: totalPosts })}</span>
|
||||
</div>
|
||||
<div className="status-bar-item">
|
||||
<span>{media.length} media</span>
|
||||
<span>{t('statusBar.media', { count: media.length })}</span>
|
||||
</div>
|
||||
|
||||
<div className="status-bar-item theme-badge">
|
||||
<span>Theme: {activeTheme}</span>
|
||||
<span>{t('statusBar.theme', { theme: activeTheme })}</span>
|
||||
</div>
|
||||
|
||||
<div className="status-bar-item language-badge">
|
||||
<span>UI</span>
|
||||
<span>{t('statusBar.ui')}</span>
|
||||
<select
|
||||
className="status-bar-language-select"
|
||||
data-testid="statusbar-language-select"
|
||||
aria-label="UI language"
|
||||
aria-label={t('statusBar.uiLanguage')}
|
||||
value={language}
|
||||
onChange={(event) => setLanguage(event.target.value as UiLanguage)}
|
||||
>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { showToast } from '../Toast';
|
||||
import { getContrastColor } from '../../utils/color';
|
||||
import { subscribeToTagEvents } from '../../utils/tagEventSubscriptions';
|
||||
import { useI18n } from '../../i18n';
|
||||
import './TagInput.css';
|
||||
|
||||
interface TagData {
|
||||
@@ -30,6 +31,7 @@ export const TagInput: React.FC<TagInputProps> = ({
|
||||
disabled = false,
|
||||
mode = 'tag',
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [suggestions, setSuggestions] = useState<TagData[]>([]);
|
||||
const [allTags, setAllTags] = useState<TagData[]>([]);
|
||||
@@ -108,7 +110,7 @@ export const TagInput: React.FC<TagInputProps> = ({
|
||||
if (!normalized) return;
|
||||
|
||||
if (value.includes(normalized)) {
|
||||
showToast.info('Tag already added');
|
||||
showToast.info(t('tagInput.alreadyAdded'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -143,7 +145,9 @@ export const TagInput: React.FC<TagInputProps> = ({
|
||||
await window.electronAPI?.tags.create({ name: normalized });
|
||||
}
|
||||
addTag(normalized);
|
||||
showToast.success(`${mode === 'category' ? 'Category' : 'Tag'} "${normalized}" created`);
|
||||
showToast.success(
|
||||
t(mode === 'category' ? 'tagInput.createdCategory' : 'tagInput.createdTag', { name: normalized })
|
||||
);
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
showToast.error(err.message);
|
||||
@@ -238,7 +242,7 @@ export const TagInput: React.FC<TagInputProps> = ({
|
||||
className="tag-chip-remove"
|
||||
onClick={() => removeTag(tagName)}
|
||||
tabIndex={-1}
|
||||
aria-label={`Remove ${tagName}`}
|
||||
aria-label={t('tagInput.remove', { tag: tagName })}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
@@ -302,7 +306,11 @@ export const TagInput: React.FC<TagInputProps> = ({
|
||||
onClick={() => createAndAddTag(inputValue.trim())}
|
||||
>
|
||||
<span className="tag-suggestion-icon">+</span>
|
||||
<span>Create {mode === 'category' ? 'category' : 'tag'} "{inputValue.trim()}"</span>
|
||||
<span>
|
||||
{t(mode === 'category' ? 'tagInput.createCategory' : 'tagInput.createTag', {
|
||||
name: inputValue.trim(),
|
||||
})}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@ type WindowControlsOverlayLike = {
|
||||
};
|
||||
|
||||
export const WindowTitleBar: React.FC = () => {
|
||||
const { language } = useI18n();
|
||||
const { language, t } = useI18n();
|
||||
const { sidebarVisible, panelVisible, toggleSidebar, togglePanel } = useAppStore();
|
||||
const [windowTitle, setWindowTitle] = useState<string>(document.title || 'Blogging Desktop Server');
|
||||
const [openMenu, setOpenMenu] = useState<{ label: string; left: number } | null>(null);
|
||||
@@ -430,9 +430,9 @@ export const WindowTitleBar: React.FC = () => {
|
||||
<div className="window-titlebar-actions">
|
||||
<button
|
||||
className="window-titlebar-action-button"
|
||||
aria-label="Toggle Sidebar"
|
||||
aria-label={t('windowTitleBar.toggleSidebar')}
|
||||
onClick={toggleSidebar}
|
||||
title={`${sidebarVisible ? 'Hide' : 'Show'} Sidebar (Ctrl+B)`}
|
||||
title={sidebarVisible ? t('windowTitleBar.hideSidebar') : t('windowTitleBar.showSidebar')}
|
||||
>
|
||||
<span
|
||||
className={`window-titlebar-sidebar-icon ${sidebarVisible ? 'is-active' : 'is-inactive'}`}
|
||||
@@ -444,9 +444,9 @@ export const WindowTitleBar: React.FC = () => {
|
||||
</button>
|
||||
<button
|
||||
className="window-titlebar-action-button"
|
||||
aria-label="Toggle Panel"
|
||||
aria-label={t('windowTitleBar.togglePanel')}
|
||||
onClick={togglePanel}
|
||||
title={`${panelVisible ? 'Hide' : 'Show'} Panel (Ctrl+J)`}
|
||||
title={panelVisible ? t('windowTitleBar.hidePanel') : t('windowTitleBar.showPanel')}
|
||||
>
|
||||
<span
|
||||
className={`window-titlebar-panel-icon ${panelVisible ? 'is-active' : 'is-inactive'}`}
|
||||
|
||||
@@ -556,5 +556,216 @@
|
||||
"panel.error.loadPostLinks": "Beitragslinks konnten nicht geladen werden.",
|
||||
"panel.error.loadGitLog": "Git-Log konnte nicht geladen werden.",
|
||||
"panel.direction.from": "von",
|
||||
"panel.direction.to": "zu"
|
||||
"panel.direction.to": "zu",
|
||||
"settings.editor.description": "Konfiguriere Verhalten und Darstellung des Beitragseditors.",
|
||||
"settings.editor.defaultModeLabel": "Standard-Editor-Modus",
|
||||
"settings.editor.defaultModeDescription": "Wähle den Standardmodus beim Öffnen von Beiträgen. Du kannst den Modus jederzeit über die Editorleiste wechseln.",
|
||||
"settings.editor.diffViewStyleLabel": "Diff-Ansichtsstil",
|
||||
"settings.editor.diffViewStyleDescription": "Wähle, wie Git-Diffs standardmäßig angezeigt werden.",
|
||||
"settings.editor.wrapLongLinesLabel": "Lange Zeilen im Diff umbrechen",
|
||||
"settings.editor.wrapLongLinesDescription": "Aktiviert Zeilenumbruch für lange Zeilen in Git-Diffs.",
|
||||
"settings.editor.wrapLongLinesAria": "Lange Zeilen im Diff umbrechen",
|
||||
"settings.editor.hideUnchangedRegionsLabel": "Unveränderte Bereiche ausblenden",
|
||||
"settings.editor.hideUnchangedRegionsDescription": "Blendet unveränderte Bereiche in Git-Diffs ein.",
|
||||
"settings.editor.hideUnchangedRegionsAria": "Unveränderte Bereiche ausblenden",
|
||||
"settings.content.newCategoryPlaceholder": "Neuer Kategoriename...",
|
||||
"settings.content.addCategory": "Kategorie hinzufügen",
|
||||
"settings.content.resetDefaults": "Auf Standard zurücksetzen",
|
||||
"settings.ai.apiKeyLabel": "OpenCode-API-Schlüssel",
|
||||
"settings.ai.apiKeyDescription": "Dein API-Schlüssel für das OpenCode-Zen-Gateway. Für KI-Funktionen erforderlich.",
|
||||
"settings.ai.apiKeyConfigured": "API-Schlüssel konfiguriert",
|
||||
"settings.ai.configured": "✓ Konfiguriert",
|
||||
"settings.ai.changeApiKey": "API-Schlüssel ändern",
|
||||
"settings.ai.defaultModelLabel": "Standardmodell",
|
||||
"settings.ai.defaultModelDescription": "Das KI-Modell für neue Chat-Unterhaltungen.",
|
||||
"settings.ai.systemPromptLabel": "System-Prompt",
|
||||
"settings.ai.systemPromptDescription": "Anweisungen für die KI zu Beginn jeder Unterhaltung. Sie bestimmen Verhalten und verfügbare Werkzeuge.",
|
||||
"settings.ai.systemPromptPlaceholder": "Systemanweisungen für den KI-Assistenten eingeben...",
|
||||
"settings.ai.savePrompt": "Prompt speichern",
|
||||
"settings.ai.resetPrompt": "Auf Standard zurücksetzen",
|
||||
"settings.publishing.ftpHostDescription": "Hostname oder IP-Adresse des FTP-Servers.",
|
||||
"settings.publishing.ftpUsernameDescription": "Benutzername deines FTP-Kontos.",
|
||||
"settings.publishing.ftpPasswordDescription": "Passwort deines FTP-Kontos.",
|
||||
"settings.publishing.showPassword": "Passwort anzeigen",
|
||||
"settings.publishing.hidePassword": "Passwort verbergen",
|
||||
"settings.publishing.sshHostDescription": "Hostname oder IP-Adresse des SSH-Servers.",
|
||||
"settings.publishing.sshUsernameDescription": "Benutzername deines SSH-Kontos.",
|
||||
"settings.publishing.sshKeyPathDescription": "Pfad zu deiner privaten SSH-Schlüsseldatei.",
|
||||
"settings.data.description": "Baut den lokalen Datenbankindex aus den Quelldateien neu auf. Nützlich bei extern bearbeiteten Dateien.",
|
||||
"settings.data.rebuildPostsLabel": "Beitragsdatenbank neu aufbauen",
|
||||
"settings.data.rebuildPostsDescription": "Alle Markdown-Beiträge neu scannen und den Datenbankindex neu aufbauen.",
|
||||
"settings.data.rebuildPostsAction": "Beiträge neu aufbauen",
|
||||
"sidebar.chat.header": "KI-ASSISTENT",
|
||||
"sidebar.chat.newChat": "Neuer Chat",
|
||||
"sidebar.chat.apiKeyNeeded": "API-Schlüssel erforderlich. Öffne einen Chat zur Konfiguration.",
|
||||
"sidebar.chat.noConversations": "Noch keine Unterhaltungen",
|
||||
"sidebar.chat.startNew": "Neuen Chat starten",
|
||||
"sidebar.chat.deleteConversation": "Unterhaltung löschen",
|
||||
"sidebar.chat.createFailed": "Neuer Chat konnte nicht erstellt werden",
|
||||
"sidebar.chat.deleteFailed": "Chat konnte nicht gelöscht werden",
|
||||
"sidebar.chat.yesterday": "Gestern",
|
||||
"sidebar.import.header": "IMPORTE",
|
||||
"sidebar.import.newDefinition": "Neue Importdefinition",
|
||||
"sidebar.import.none": "Noch keine Importdefinitionen",
|
||||
"sidebar.import.createDefinition": "Eine Importdefinition erstellen",
|
||||
"sidebar.import.deleteDefinition": "Importdefinition löschen",
|
||||
"sidebar.import.createFailed": "Importdefinition konnte nicht erstellt werden",
|
||||
"sidebar.import.deleteFailed": "Importdefinition konnte nicht gelöscht werden",
|
||||
"editor.error.saveTitle": "Speichern fehlgeschlagen",
|
||||
"editor.error.saveMessage": "Beitrag konnte nicht gespeichert werden",
|
||||
"editor.error.publishTitle": "Veröffentlichen fehlgeschlagen",
|
||||
"editor.error.publishMessage": "Beitrag konnte nicht veröffentlicht werden",
|
||||
"editor.error.discardTitle": "Verwerfen fehlgeschlagen",
|
||||
"editor.error.deleteTitle": "Löschen fehlgeschlagen",
|
||||
"editor.error.operationMessage": "Vorgang fehlgeschlagen",
|
||||
"editor.error.deletePostMessage": "Beitrag konnte nicht gelöscht werden",
|
||||
"editor.error.fetchPostReferencesMessage": "Beitragsreferenzen konnten nicht geladen werden",
|
||||
"editor.confirm.discardChanges": "Alle Änderungen seit der letzten Veröffentlichung verwerfen? Das kann nicht rückgängig gemacht werden.",
|
||||
"editor.confirm.deleteDraft": "Diesen Entwurf löschen? Das kann nicht rückgängig gemacht werden.",
|
||||
"editor.toast.published": "Beitrag veröffentlicht",
|
||||
"editor.toast.reverted": "Auf letzte veröffentlichte Version zurückgesetzt",
|
||||
"editor.toast.draftDeleted": "Entwurf gelöscht",
|
||||
"editor.toast.postDeleted": "Beitrag gelöscht",
|
||||
"editor.media.notFound": "Medium nicht gefunden",
|
||||
"editor.media.error.analyzeImage": "Bildanalyse fehlgeschlagen",
|
||||
"editor.media.error.updateTitle": "Aktualisierung fehlgeschlagen",
|
||||
"editor.media.error.updateMessage": "Medium konnte nicht aktualisiert werden",
|
||||
"editor.media.error.replaceTitle": "Ersetzen fehlgeschlagen",
|
||||
"editor.media.error.replaceMessage": "Mediendatei konnte nicht ersetzt werden",
|
||||
"editor.media.error.deleteMessage": "Medium konnte nicht gelöscht werden",
|
||||
"editor.media.error.fetchReferencesMessage": "Medienreferenzen konnten nicht geladen werden",
|
||||
"editor.media.toast.aiApplied": "KI-Vorschläge übernommen",
|
||||
"editor.media.toast.linkedToPost": "Mit Beitrag verknüpft",
|
||||
"editor.media.toast.linkFailed": "Verknüpfung mit Beitrag fehlgeschlagen",
|
||||
"editor.media.toast.unlinkedFromPost": "Vom Beitrag getrennt",
|
||||
"editor.media.toast.unlinkFailed": "Trennen vom Beitrag fehlgeschlagen",
|
||||
"editor.media.toast.updated": "Medium aktualisiert",
|
||||
"editor.media.toast.fileReplaced": "Datei ersetzt (Vorschaubilder neu erstellt)",
|
||||
"editor.media.toast.deleted": "Medium gelöscht",
|
||||
"editor.media.quickActions.title": "Schnellaktionen",
|
||||
"editor.media.quickActions.analyzing": "⏳ Analysiere...",
|
||||
"editor.media.quickActions.button": "⚡ Schnellaktionen",
|
||||
"editor.media.quickActions.aiTitle": "KI: Titel, Alt-Text und Bildunterschrift erzeugen",
|
||||
"editor.media.quickActions.aiDescription": "Analysiert das Bild und schlägt Metadaten vor",
|
||||
"editor.media.replaceFile": "Datei ersetzen",
|
||||
"editor.media.field.fileName": "Dateiname",
|
||||
"editor.media.field.type": "Typ",
|
||||
"editor.media.field.size": "Größe",
|
||||
"editor.media.field.dimensions": "Abmessungen",
|
||||
"editor.media.field.title": "Titel",
|
||||
"editor.media.field.altText": "Alternativtext",
|
||||
"editor.media.field.caption": "Bildunterschrift",
|
||||
"editor.media.field.tags": "Tags (kommagetrennt)",
|
||||
"editor.media.field.author": "Autor",
|
||||
"editor.media.placeholder.title": "Titel für Listen und Suchergebnisse",
|
||||
"editor.media.placeholder.altText": "Bild für Barrierefreiheit beschreiben",
|
||||
"editor.media.placeholder.caption": "Bildunterschrift",
|
||||
"editor.media.placeholder.tags": "tag1, tag2, tag3",
|
||||
"editor.media.placeholder.author": "Autorenname",
|
||||
"editor.media.linkedPosts": "Verknüpfte Beiträge",
|
||||
"editor.media.linkToPostTitle": "Mit einem Beitrag verknüpfen",
|
||||
"editor.media.linkAction": "+ Verknüpfen",
|
||||
"editor.media.searchPosts": "Beiträge suchen...",
|
||||
"editor.media.noMatchingPosts": "Keine passenden Beiträge",
|
||||
"editor.media.noPostsToLink": "Keine verknüpfbaren Beiträge verfügbar",
|
||||
"editor.media.morePosts": "+{count} weitere Beiträge",
|
||||
"editor.media.notLinked": "Mit keinem Beitrag verknüpft",
|
||||
"editor.media.openPost": "Beitrag öffnen",
|
||||
"editor.media.unlinkFromPost": "Vom Beitrag trennen",
|
||||
"postSearch.placeholder": "Beiträge nach Titel oder Inhalt durchsuchen...",
|
||||
"postSearch.searching": "Suche...",
|
||||
"postSearch.typeMore": "Mindestens 2 Zeichen zum Suchen eingeben",
|
||||
"postSearch.noResults": "Keine Beiträge für \"{query}\" gefunden",
|
||||
"postSearch.hint": "Mit ↑↓ navigieren, Enter auswählen, Esc schließen",
|
||||
"statusBar.posts": "{count} Beiträge",
|
||||
"statusBar.media": "{count} Medien",
|
||||
"statusBar.theme": "Theme: {theme}",
|
||||
"statusBar.ui": "UI",
|
||||
"statusBar.uiLanguage": "UI-Sprache",
|
||||
"windowTitleBar.toggleSidebar": "Seitenleiste umschalten",
|
||||
"windowTitleBar.hideSidebar": "Seitenleiste ausblenden (Ctrl+B)",
|
||||
"windowTitleBar.showSidebar": "Seitenleiste anzeigen (Ctrl+B)",
|
||||
"windowTitleBar.togglePanel": "Panel umschalten",
|
||||
"windowTitleBar.hidePanel": "Panel ausblenden (Ctrl+J)",
|
||||
"windowTitleBar.showPanel": "Panel anzeigen (Ctrl+J)",
|
||||
"tagInput.alreadyAdded": "Tag bereits hinzugefügt",
|
||||
"tagInput.remove": "{tag} entfernen",
|
||||
"tagInput.createdTag": "Tag \"{name}\" erstellt",
|
||||
"tagInput.createdCategory": "Kategorie \"{name}\" erstellt",
|
||||
"tagInput.createTag": "Tag \"{name}\" erstellen",
|
||||
"tagInput.createCategory": "Kategorie \"{name}\" erstellen",
|
||||
"importAnalysis.loadingDefinition": "Importdefinition wird geladen...",
|
||||
"importAnalysis.namePlaceholder": "Importname...",
|
||||
"importAnalysis.headerDescription": "Wähle eine WordPress-Exportdatei (WXR) und einen Upload-Ordner, um den Import zu analysieren.",
|
||||
"importAnalysis.uploadsFolder": "Uploads-Ordner",
|
||||
"importAnalysis.noFolderSelected": "Kein Ordner ausgewählt",
|
||||
"importAnalysis.wxrFile": "WXR-Datei",
|
||||
"importAnalysis.selectFileToAnalyze": "Datei zur Analyse auswählen",
|
||||
"importAnalysis.analyzing": "Analysiere...",
|
||||
"importAnalysis.selectAndAnalyze": "Auswählen & analysieren",
|
||||
"importAnalysis.analyzingWxr": "WXR-Datei wird analysiert...",
|
||||
"importAnalysis.emptyState": "Wähle eine WordPress-Exportdatei, um die Analyse zu starten.",
|
||||
"importAnalysis.importing": "Import läuft...",
|
||||
"importAnalysis.importComplete": "Import erfolgreich abgeschlossen!",
|
||||
"importAnalysis.importFailed": "Import fehlgeschlagen: {error}",
|
||||
"importAnalysis.untitledImport": "Unbenannter Import",
|
||||
"importAnalysis.executionStarting": "Starte...",
|
||||
"importAnalysis.unknownError": "Unbekannter Fehler",
|
||||
"importAnalysis.readyToImport": "Bereit zum Import:",
|
||||
"importAnalysis.tagsCategories": "Tags/Kategorien",
|
||||
"importAnalysis.posts": "Beiträge",
|
||||
"importAnalysis.media": "Medien",
|
||||
"importAnalysis.pages": "Seiten",
|
||||
"importAnalysis.nothingToImport": "Nichts zu importieren",
|
||||
"importAnalysis.importItems": "{count} Elemente importieren",
|
||||
"importAnalysis.postSlugConflicts": "Beitrags-Slug-Konflikte",
|
||||
"importAnalysis.pageSlugConflicts": "Seiten-Slug-Konflikte",
|
||||
"importAnalysis.postsWithCount": "Beiträge ({count})",
|
||||
"importAnalysis.otherWithCount": "Andere ({count})",
|
||||
"importAnalysis.pagesWithCount": "Seiten ({count})",
|
||||
"importAnalysis.mediaWithCount": "Medien ({count})",
|
||||
"importAnalysis.site": "Website",
|
||||
"importAnalysis.untitled": "Ohne Titel",
|
||||
"importAnalysis.url": "URL",
|
||||
"importAnalysis.language": "Sprache",
|
||||
"importAnalysis.file": "Datei",
|
||||
"importAnalysis.notAvailable": "k. A.",
|
||||
"importAnalysis.new": "neu",
|
||||
"importAnalysis.update": "Aktualisierung",
|
||||
"importAnalysis.conflict": "Konflikt",
|
||||
"importAnalysis.duplicate": "Duplikat",
|
||||
"importAnalysis.missing": "fehlend",
|
||||
"importAnalysis.categories": "Kategorien",
|
||||
"importAnalysis.existing": "vorhanden",
|
||||
"importAnalysis.mapped": "zugeordnet",
|
||||
"importAnalysis.tags": "Tags",
|
||||
"importAnalysis.dateDistribution": "Datumsverteilung",
|
||||
"importAnalysis.postsPages": "Beiträge/Seiten",
|
||||
"importAnalysis.total": "gesamt",
|
||||
"importAnalysis.wordpressId": "WordPress-ID",
|
||||
"importAnalysis.type": "Typ",
|
||||
"importAnalysis.author": "Autor",
|
||||
"importAnalysis.unknown": "Unbekannt",
|
||||
"importAnalysis.published": "Veröffentlicht",
|
||||
"importAnalysis.excerpt": "Auszug",
|
||||
"importAnalysis.content": "Inhalt",
|
||||
"importAnalysis.loading": "Lade...",
|
||||
"importAnalysis.mimeType": "MIME-Typ",
|
||||
"importAnalysis.uploaded": "Hochgeladen",
|
||||
"importAnalysis.parentPostId": "Elternbeitrags-ID",
|
||||
"importAnalysis.description": "Beschreibung",
|
||||
"importAnalysis.slug": "Slug",
|
||||
"importAnalysis.newEntryWxr": "Neuer Eintrag (WXR)",
|
||||
"importAnalysis.existingEntry": "Vorhandener Eintrag",
|
||||
"importAnalysis.resolution": "Lösung",
|
||||
"importAnalysis.ignore": "Ignorieren",
|
||||
"importAnalysis.overwrite": "Überschreiben",
|
||||
"importAnalysis.importNewSlug": "Importieren (neuer Slug)",
|
||||
"importAnalysis.status": "Status",
|
||||
"importAnalysis.title": "Titel",
|
||||
"importAnalysis.wpStatus": "WP-Status",
|
||||
"importAnalysis.existingMatch": "Vorhandene Übereinstimmung",
|
||||
"importAnalysis.none": "--",
|
||||
"importAnalysis.filename": "Dateiname",
|
||||
"importAnalysis.path": "Pfad"
|
||||
}
|
||||
|
||||
@@ -556,5 +556,216 @@
|
||||
"panel.error.loadPostLinks": "Failed to load post links.",
|
||||
"panel.error.loadGitLog": "Failed to load git log.",
|
||||
"panel.direction.from": "from",
|
||||
"panel.direction.to": "to"
|
||||
"panel.direction.to": "to",
|
||||
"settings.editor.description": "Configure the blog post editor behavior and appearance.",
|
||||
"settings.editor.defaultModeLabel": "Default Editor Mode",
|
||||
"settings.editor.defaultModeDescription": "Choose the default mode when opening posts. You can switch modes at any time using the editor toolbar.",
|
||||
"settings.editor.diffViewStyleLabel": "Diff View Style",
|
||||
"settings.editor.diffViewStyleDescription": "Choose how Git diffs are shown by default.",
|
||||
"settings.editor.wrapLongLinesLabel": "Wrap Long Lines in Diff",
|
||||
"settings.editor.wrapLongLinesDescription": "Enable word wrapping for long lines in Git diffs.",
|
||||
"settings.editor.wrapLongLinesAria": "Wrap long lines in diff",
|
||||
"settings.editor.hideUnchangedRegionsLabel": "Hide Unchanged Regions",
|
||||
"settings.editor.hideUnchangedRegionsDescription": "Collapse unchanged regions in Git diffs.",
|
||||
"settings.editor.hideUnchangedRegionsAria": "Hide unchanged regions",
|
||||
"settings.content.newCategoryPlaceholder": "New category name...",
|
||||
"settings.content.addCategory": "Add Category",
|
||||
"settings.content.resetDefaults": "Reset to Defaults",
|
||||
"settings.ai.apiKeyLabel": "OpenCode API Key",
|
||||
"settings.ai.apiKeyDescription": "Your API key for the OpenCode Zen gateway. Required to use AI features.",
|
||||
"settings.ai.apiKeyConfigured": "API key configured",
|
||||
"settings.ai.configured": "✓ Configured",
|
||||
"settings.ai.changeApiKey": "Change API Key",
|
||||
"settings.ai.defaultModelLabel": "Default Model",
|
||||
"settings.ai.defaultModelDescription": "The AI model to use for new chat conversations.",
|
||||
"settings.ai.systemPromptLabel": "System Prompt",
|
||||
"settings.ai.systemPromptDescription": "Instructions given to the AI at the start of each conversation. This defines how the assistant behaves and what tools it knows about.",
|
||||
"settings.ai.systemPromptPlaceholder": "Enter system instructions for the AI assistant...",
|
||||
"settings.ai.savePrompt": "Save Prompt",
|
||||
"settings.ai.resetPrompt": "Reset to Default",
|
||||
"settings.publishing.ftpHostDescription": "The FTP server hostname or IP address.",
|
||||
"settings.publishing.ftpUsernameDescription": "Your FTP account username.",
|
||||
"settings.publishing.ftpPasswordDescription": "Your FTP account password.",
|
||||
"settings.publishing.showPassword": "Show password",
|
||||
"settings.publishing.hidePassword": "Hide password",
|
||||
"settings.publishing.sshHostDescription": "The SSH server hostname or IP address.",
|
||||
"settings.publishing.sshUsernameDescription": "Your SSH account username.",
|
||||
"settings.publishing.sshKeyPathDescription": "Path to your SSH private key file.",
|
||||
"settings.data.description": "Rebuild the local database index from source files. Useful if post or media files were edited externally.",
|
||||
"settings.data.rebuildPostsLabel": "Rebuild Posts Database",
|
||||
"settings.data.rebuildPostsDescription": "Re-scan all post markdown files and rebuild the database index.",
|
||||
"settings.data.rebuildPostsAction": "Rebuild Posts",
|
||||
"sidebar.chat.header": "AI ASSISTANT",
|
||||
"sidebar.chat.newChat": "New Chat",
|
||||
"sidebar.chat.apiKeyNeeded": "API key needed. Open a chat to configure.",
|
||||
"sidebar.chat.noConversations": "No conversations yet",
|
||||
"sidebar.chat.startNew": "Start a new chat",
|
||||
"sidebar.chat.deleteConversation": "Delete conversation",
|
||||
"sidebar.chat.createFailed": "Failed to create new chat",
|
||||
"sidebar.chat.deleteFailed": "Failed to delete chat",
|
||||
"sidebar.chat.yesterday": "Yesterday",
|
||||
"sidebar.import.header": "IMPORTS",
|
||||
"sidebar.import.newDefinition": "New Import Definition",
|
||||
"sidebar.import.none": "No import definitions yet",
|
||||
"sidebar.import.createDefinition": "Create an import definition",
|
||||
"sidebar.import.deleteDefinition": "Delete import definition",
|
||||
"sidebar.import.createFailed": "Failed to create import definition",
|
||||
"sidebar.import.deleteFailed": "Failed to delete import definition",
|
||||
"editor.error.saveTitle": "Save Failed",
|
||||
"editor.error.saveMessage": "Failed to save post",
|
||||
"editor.error.publishTitle": "Publish Failed",
|
||||
"editor.error.publishMessage": "Failed to publish post",
|
||||
"editor.error.discardTitle": "Discard Failed",
|
||||
"editor.error.deleteTitle": "Delete Failed",
|
||||
"editor.error.operationMessage": "Operation failed",
|
||||
"editor.error.deletePostMessage": "Failed to delete post",
|
||||
"editor.error.fetchPostReferencesMessage": "Failed to fetch post references",
|
||||
"editor.confirm.discardChanges": "Discard all changes since last publish? This cannot be undone.",
|
||||
"editor.confirm.deleteDraft": "Delete this draft? This cannot be undone.",
|
||||
"editor.toast.published": "Post published",
|
||||
"editor.toast.reverted": "Reverted to last published version",
|
||||
"editor.toast.draftDeleted": "Draft deleted",
|
||||
"editor.toast.postDeleted": "Post deleted",
|
||||
"editor.media.notFound": "Media not found",
|
||||
"editor.media.error.analyzeImage": "Failed to analyze image",
|
||||
"editor.media.error.updateTitle": "Update Failed",
|
||||
"editor.media.error.updateMessage": "Failed to update media",
|
||||
"editor.media.error.replaceTitle": "Replace Failed",
|
||||
"editor.media.error.replaceMessage": "Failed to replace media file",
|
||||
"editor.media.error.deleteMessage": "Failed to delete media",
|
||||
"editor.media.error.fetchReferencesMessage": "Failed to fetch media references",
|
||||
"editor.media.toast.aiApplied": "AI suggestions applied",
|
||||
"editor.media.toast.linkedToPost": "Linked to post",
|
||||
"editor.media.toast.linkFailed": "Failed to link to post",
|
||||
"editor.media.toast.unlinkedFromPost": "Unlinked from post",
|
||||
"editor.media.toast.unlinkFailed": "Failed to unlink from post",
|
||||
"editor.media.toast.updated": "Media updated",
|
||||
"editor.media.toast.fileReplaced": "File replaced (thumbnails regenerated)",
|
||||
"editor.media.toast.deleted": "Media deleted",
|
||||
"editor.media.quickActions.title": "Quick Actions",
|
||||
"editor.media.quickActions.analyzing": "⏳ Analyzing...",
|
||||
"editor.media.quickActions.button": "⚡ Quick Actions",
|
||||
"editor.media.quickActions.aiTitle": "AI: Generate Title, Alt & Caption",
|
||||
"editor.media.quickActions.aiDescription": "Analyzes the image to suggest metadata",
|
||||
"editor.media.replaceFile": "Replace File",
|
||||
"editor.media.field.fileName": "File Name",
|
||||
"editor.media.field.type": "Type",
|
||||
"editor.media.field.size": "Size",
|
||||
"editor.media.field.dimensions": "Dimensions",
|
||||
"editor.media.field.title": "Title",
|
||||
"editor.media.field.altText": "Alt Text",
|
||||
"editor.media.field.caption": "Caption",
|
||||
"editor.media.field.tags": "Tags (comma-separated)",
|
||||
"editor.media.field.author": "Author",
|
||||
"editor.media.placeholder.title": "Title for lists and search results",
|
||||
"editor.media.placeholder.altText": "Describe the image for accessibility",
|
||||
"editor.media.placeholder.caption": "Image caption",
|
||||
"editor.media.placeholder.tags": "tag1, tag2, tag3",
|
||||
"editor.media.placeholder.author": "Author name",
|
||||
"editor.media.linkedPosts": "Linked Posts",
|
||||
"editor.media.linkToPostTitle": "Link to a post",
|
||||
"editor.media.linkAction": "+ Link",
|
||||
"editor.media.searchPosts": "Search posts...",
|
||||
"editor.media.noMatchingPosts": "No matching posts",
|
||||
"editor.media.noPostsToLink": "No posts available to link",
|
||||
"editor.media.morePosts": "+{count} more posts",
|
||||
"editor.media.notLinked": "Not linked to any posts",
|
||||
"editor.media.openPost": "Open post",
|
||||
"editor.media.unlinkFromPost": "Unlink from post",
|
||||
"postSearch.placeholder": "Search posts by title or content...",
|
||||
"postSearch.searching": "Searching...",
|
||||
"postSearch.typeMore": "Type at least 2 characters to search",
|
||||
"postSearch.noResults": "No posts found for \"{query}\"",
|
||||
"postSearch.hint": "Use ↑↓ to navigate, Enter to select, Esc to close",
|
||||
"statusBar.posts": "{count} posts",
|
||||
"statusBar.media": "{count} media",
|
||||
"statusBar.theme": "Theme: {theme}",
|
||||
"statusBar.ui": "UI",
|
||||
"statusBar.uiLanguage": "UI language",
|
||||
"windowTitleBar.toggleSidebar": "Toggle Sidebar",
|
||||
"windowTitleBar.hideSidebar": "Hide Sidebar (Ctrl+B)",
|
||||
"windowTitleBar.showSidebar": "Show Sidebar (Ctrl+B)",
|
||||
"windowTitleBar.togglePanel": "Toggle Panel",
|
||||
"windowTitleBar.hidePanel": "Hide Panel (Ctrl+J)",
|
||||
"windowTitleBar.showPanel": "Show Panel (Ctrl+J)",
|
||||
"tagInput.alreadyAdded": "Tag already added",
|
||||
"tagInput.remove": "Remove {tag}",
|
||||
"tagInput.createdTag": "Tag \"{name}\" created",
|
||||
"tagInput.createdCategory": "Category \"{name}\" created",
|
||||
"tagInput.createTag": "Create tag \"{name}\"",
|
||||
"tagInput.createCategory": "Create category \"{name}\"",
|
||||
"importAnalysis.loadingDefinition": "Loading import definition...",
|
||||
"importAnalysis.namePlaceholder": "Import name...",
|
||||
"importAnalysis.headerDescription": "Select a WordPress export file (WXR) and an uploads folder to analyze what would be imported.",
|
||||
"importAnalysis.uploadsFolder": "Uploads Folder",
|
||||
"importAnalysis.noFolderSelected": "No folder selected",
|
||||
"importAnalysis.wxrFile": "WXR File",
|
||||
"importAnalysis.selectFileToAnalyze": "Select a file to analyze",
|
||||
"importAnalysis.analyzing": "Analyzing...",
|
||||
"importAnalysis.selectAndAnalyze": "Select & Analyze",
|
||||
"importAnalysis.analyzingWxr": "Analyzing WXR file...",
|
||||
"importAnalysis.emptyState": "Select a WordPress export file to begin analysis.",
|
||||
"importAnalysis.importing": "Importing...",
|
||||
"importAnalysis.importComplete": "Import completed successfully!",
|
||||
"importAnalysis.importFailed": "Import failed: {error}",
|
||||
"importAnalysis.untitledImport": "Untitled Import",
|
||||
"importAnalysis.executionStarting": "Starting...",
|
||||
"importAnalysis.unknownError": "Unknown error",
|
||||
"importAnalysis.readyToImport": "Ready to import:",
|
||||
"importAnalysis.tagsCategories": "tags/categories",
|
||||
"importAnalysis.posts": "posts",
|
||||
"importAnalysis.media": "media",
|
||||
"importAnalysis.pages": "pages",
|
||||
"importAnalysis.nothingToImport": "Nothing to Import",
|
||||
"importAnalysis.importItems": "Import {count} Items",
|
||||
"importAnalysis.postSlugConflicts": "Post Slug Conflicts",
|
||||
"importAnalysis.pageSlugConflicts": "Page Slug Conflicts",
|
||||
"importAnalysis.postsWithCount": "Posts ({count})",
|
||||
"importAnalysis.otherWithCount": "Other ({count})",
|
||||
"importAnalysis.pagesWithCount": "Pages ({count})",
|
||||
"importAnalysis.mediaWithCount": "Media ({count})",
|
||||
"importAnalysis.site": "Site",
|
||||
"importAnalysis.untitled": "Untitled",
|
||||
"importAnalysis.url": "URL",
|
||||
"importAnalysis.language": "Language",
|
||||
"importAnalysis.file": "File",
|
||||
"importAnalysis.notAvailable": "N/A",
|
||||
"importAnalysis.new": "new",
|
||||
"importAnalysis.update": "update",
|
||||
"importAnalysis.conflict": "conflict",
|
||||
"importAnalysis.duplicate": "duplicate",
|
||||
"importAnalysis.missing": "missing",
|
||||
"importAnalysis.categories": "Categories",
|
||||
"importAnalysis.existing": "existing",
|
||||
"importAnalysis.mapped": "mapped",
|
||||
"importAnalysis.tags": "Tags",
|
||||
"importAnalysis.dateDistribution": "Date Distribution",
|
||||
"importAnalysis.postsPages": "Posts/Pages",
|
||||
"importAnalysis.total": "total",
|
||||
"importAnalysis.wordpressId": "WordPress ID",
|
||||
"importAnalysis.type": "Type",
|
||||
"importAnalysis.author": "Author",
|
||||
"importAnalysis.unknown": "Unknown",
|
||||
"importAnalysis.published": "Published",
|
||||
"importAnalysis.excerpt": "Excerpt",
|
||||
"importAnalysis.content": "Content",
|
||||
"importAnalysis.loading": "Loading...",
|
||||
"importAnalysis.mimeType": "MIME Type",
|
||||
"importAnalysis.uploaded": "Uploaded",
|
||||
"importAnalysis.parentPostId": "Parent Post ID",
|
||||
"importAnalysis.description": "Description",
|
||||
"importAnalysis.slug": "Slug",
|
||||
"importAnalysis.newEntryWxr": "New Entry (WXR)",
|
||||
"importAnalysis.existingEntry": "Existing Entry",
|
||||
"importAnalysis.resolution": "Resolution",
|
||||
"importAnalysis.ignore": "Ignore",
|
||||
"importAnalysis.overwrite": "Overwrite",
|
||||
"importAnalysis.importNewSlug": "Import (new slug)",
|
||||
"importAnalysis.status": "Status",
|
||||
"importAnalysis.title": "Title",
|
||||
"importAnalysis.wpStatus": "WP Status",
|
||||
"importAnalysis.existingMatch": "Existing Match",
|
||||
"importAnalysis.none": "--",
|
||||
"importAnalysis.filename": "Filename",
|
||||
"importAnalysis.path": "Path"
|
||||
}
|
||||
|
||||
@@ -556,5 +556,216 @@
|
||||
"panel.error.loadPostLinks": "No se pudieron cargar los enlaces de la entrada.",
|
||||
"panel.error.loadGitLog": "No se pudo cargar el registro Git.",
|
||||
"panel.direction.from": "desde",
|
||||
"panel.direction.to": "hacia"
|
||||
"panel.direction.to": "hacia",
|
||||
"settings.editor.description": "Personaliza el comportamiento y la apariencia del editor.",
|
||||
"settings.editor.defaultModeLabel": "Modo predeterminado",
|
||||
"settings.editor.defaultModeDescription": "Elige cómo se abre el editor por defecto.",
|
||||
"settings.editor.diffViewStyleLabel": "Estilo de vista diff",
|
||||
"settings.editor.diffViewStyleDescription": "Define cómo se muestran las diferencias.",
|
||||
"settings.editor.wrapLongLinesLabel": "Ajustar líneas largas",
|
||||
"settings.editor.wrapLongLinesDescription": "Ajusta automáticamente las líneas largas.",
|
||||
"settings.editor.wrapLongLinesAria": "Activar ajuste de línea",
|
||||
"settings.editor.hideUnchangedRegionsLabel": "Ocultar regiones sin cambios",
|
||||
"settings.editor.hideUnchangedRegionsDescription": "Contrae las secciones sin cambios en la vista diff.",
|
||||
"settings.editor.hideUnchangedRegionsAria": "Activar ocultar regiones sin cambios",
|
||||
"settings.content.newCategoryPlaceholder": "Nueva categoría",
|
||||
"settings.content.addCategory": "Añadir categoría",
|
||||
"settings.content.resetDefaults": "Restablecer valores predeterminados",
|
||||
"settings.ai.apiKeyLabel": "Clave API",
|
||||
"settings.ai.apiKeyDescription": "Introduce tu clave API para habilitar funciones de IA.",
|
||||
"settings.ai.apiKeyConfigured": "Clave API configurada",
|
||||
"settings.ai.configured": "Configurado",
|
||||
"settings.ai.changeApiKey": "Cambiar clave API",
|
||||
"settings.ai.defaultModelLabel": "Modelo predeterminado",
|
||||
"settings.ai.defaultModelDescription": "Selecciona el modelo de IA usado por defecto.",
|
||||
"settings.ai.systemPromptLabel": "Prompt del sistema",
|
||||
"settings.ai.systemPromptDescription": "Define las instrucciones del sistema enviadas al modelo.",
|
||||
"settings.ai.systemPromptPlaceholder": "Escribe aquí tu prompt del sistema…",
|
||||
"settings.ai.savePrompt": "Guardar prompt",
|
||||
"settings.ai.resetPrompt": "Restablecer prompt",
|
||||
"settings.publishing.ftpHostDescription": "Nombre de host o IP del servidor FTP.",
|
||||
"settings.publishing.ftpUsernameDescription": "Nombre de usuario de FTP.",
|
||||
"settings.publishing.ftpPasswordDescription": "Contraseña de FTP.",
|
||||
"settings.publishing.showPassword": "Mostrar contraseña",
|
||||
"settings.publishing.hidePassword": "Ocultar contraseña",
|
||||
"settings.publishing.sshHostDescription": "Nombre de host o IP del servidor SSH.",
|
||||
"settings.publishing.sshUsernameDescription": "Nombre de usuario de SSH.",
|
||||
"settings.publishing.sshKeyPathDescription": "Ruta a tu clave privada SSH.",
|
||||
"settings.data.description": "Gestiona y reconstruye los datos locales.",
|
||||
"settings.data.rebuildPostsLabel": "Reconstruir índice de publicaciones",
|
||||
"settings.data.rebuildPostsDescription": "Escanea todas las publicaciones y actualiza el índice de datos.",
|
||||
"settings.data.rebuildPostsAction": "Reconstruir",
|
||||
"sidebar.chat.header": "Chat",
|
||||
"sidebar.chat.newChat": "Nuevo chat",
|
||||
"sidebar.chat.apiKeyNeeded": "Se necesita una clave API para usar el chat.",
|
||||
"sidebar.chat.noConversations": "Sin conversaciones",
|
||||
"sidebar.chat.startNew": "Iniciar una nueva conversación",
|
||||
"sidebar.chat.deleteConversation": "Eliminar conversación",
|
||||
"sidebar.chat.createFailed": "No se pudo crear la conversación: {error}",
|
||||
"sidebar.chat.deleteFailed": "No se pudo eliminar la conversación: {error}",
|
||||
"sidebar.chat.yesterday": "Ayer",
|
||||
"sidebar.import.header": "Importación",
|
||||
"sidebar.import.newDefinition": "Nueva definición",
|
||||
"sidebar.import.none": "Sin definiciones de importación",
|
||||
"sidebar.import.createDefinition": "Crear definición",
|
||||
"sidebar.import.deleteDefinition": "Eliminar definición",
|
||||
"sidebar.import.createFailed": "No se pudo crear la definición: {error}",
|
||||
"sidebar.import.deleteFailed": "No se pudo eliminar la definición: {error}",
|
||||
"editor.error.saveTitle": "Error al guardar",
|
||||
"editor.error.saveMessage": "No se pudo guardar la publicación: {error}",
|
||||
"editor.error.publishTitle": "Error al publicar",
|
||||
"editor.error.publishMessage": "No se pudo publicar la publicación: {error}",
|
||||
"editor.error.discardTitle": "Error al descartar",
|
||||
"editor.error.deleteTitle": "Error al eliminar",
|
||||
"editor.error.operationMessage": "La operación falló: {error}",
|
||||
"editor.error.deletePostMessage": "No se pudo eliminar la publicación: {error}",
|
||||
"editor.error.fetchPostReferencesMessage": "No se pudieron obtener las referencias de la publicación: {error}",
|
||||
"editor.confirm.discardChanges": "¿Descartar los cambios sin guardar?",
|
||||
"editor.confirm.deleteDraft": "¿Eliminar este borrador?",
|
||||
"editor.toast.published": "Publicación publicada",
|
||||
"editor.toast.reverted": "Cambios revertidos",
|
||||
"editor.toast.draftDeleted": "Borrador eliminado",
|
||||
"editor.toast.postDeleted": "Publicación eliminada",
|
||||
"editor.media.notFound": "Medio no encontrado",
|
||||
"editor.media.error.analyzeImage": "No se pudo analizar la imagen: {error}",
|
||||
"editor.media.error.updateTitle": "Error al actualizar",
|
||||
"editor.media.error.updateMessage": "No se pudo actualizar el medio: {error}",
|
||||
"editor.media.error.replaceTitle": "Error al reemplazar",
|
||||
"editor.media.error.replaceMessage": "No se pudo reemplazar el archivo de medios: {error}",
|
||||
"editor.media.error.deleteMessage": "No se pudo eliminar el medio: {error}",
|
||||
"editor.media.error.fetchReferencesMessage": "No se pudieron obtener las referencias del medio: {error}",
|
||||
"editor.media.toast.aiApplied": "Sugerencias de IA aplicadas",
|
||||
"editor.media.toast.linkedToPost": "Medio vinculado a la publicación",
|
||||
"editor.media.toast.linkFailed": "No se pudo vincular el medio: {error}",
|
||||
"editor.media.toast.unlinkedFromPost": "Medio desvinculado de la publicación",
|
||||
"editor.media.toast.unlinkFailed": "No se pudo desvincular el medio: {error}",
|
||||
"editor.media.toast.updated": "Medio actualizado",
|
||||
"editor.media.toast.fileReplaced": "Archivo de medios reemplazado",
|
||||
"editor.media.toast.deleted": "Medio eliminado",
|
||||
"editor.media.quickActions.title": "Acciones rápidas",
|
||||
"editor.media.quickActions.analyzing": "🔎 Analizando…",
|
||||
"editor.media.quickActions.button": "✨ Analizar con IA",
|
||||
"editor.media.quickActions.aiTitle": "Título sugerido por IA",
|
||||
"editor.media.quickActions.aiDescription": "Genera automáticamente título, texto alternativo y pie de foto.",
|
||||
"editor.media.replaceFile": "Reemplazar archivo",
|
||||
"editor.media.field.fileName": "Nombre de archivo",
|
||||
"editor.media.field.type": "Tipo",
|
||||
"editor.media.field.size": "Tamaño",
|
||||
"editor.media.field.dimensions": "Dimensiones",
|
||||
"editor.media.field.title": "Título",
|
||||
"editor.media.field.altText": "Texto alternativo",
|
||||
"editor.media.field.caption": "Pie de foto",
|
||||
"editor.media.field.tags": "Etiquetas",
|
||||
"editor.media.field.author": "Autor",
|
||||
"editor.media.placeholder.title": "Introduce un título",
|
||||
"editor.media.placeholder.altText": "Describe la imagen para accesibilidad",
|
||||
"editor.media.placeholder.caption": "Añadir pie de foto",
|
||||
"editor.media.placeholder.tags": "Añadir etiquetas",
|
||||
"editor.media.placeholder.author": "Nombre del autor",
|
||||
"editor.media.linkedPosts": "Publicaciones vinculadas",
|
||||
"editor.media.linkToPostTitle": "Vincular a una publicación",
|
||||
"editor.media.linkAction": "Vincular",
|
||||
"editor.media.searchPosts": "Buscar publicaciones",
|
||||
"editor.media.noMatchingPosts": "No hay publicaciones que coincidan con “{query}”",
|
||||
"editor.media.noPostsToLink": "No hay publicaciones disponibles para vincular",
|
||||
"editor.media.morePosts": "{count} publicaciones más",
|
||||
"editor.media.notLinked": "No vinculado",
|
||||
"editor.media.openPost": "Abrir publicación",
|
||||
"editor.media.unlinkFromPost": "Desvincular de la publicación",
|
||||
"postSearch.placeholder": "Buscar publicaciones…",
|
||||
"postSearch.searching": "Buscando…",
|
||||
"postSearch.typeMore": "Escribe más caracteres para buscar",
|
||||
"postSearch.noResults": "Sin resultados para “{query}”",
|
||||
"postSearch.hint": "Busca por título, slug o contenido",
|
||||
"statusBar.posts": "Publicaciones",
|
||||
"statusBar.media": "Medios",
|
||||
"statusBar.theme": "Tema: {theme}",
|
||||
"statusBar.ui": "UI",
|
||||
"statusBar.uiLanguage": "Idioma de la interfaz",
|
||||
"windowTitleBar.toggleSidebar": "Alternar barra lateral",
|
||||
"windowTitleBar.hideSidebar": "Ocultar barra lateral",
|
||||
"windowTitleBar.showSidebar": "Mostrar barra lateral",
|
||||
"windowTitleBar.togglePanel": "Alternar panel",
|
||||
"windowTitleBar.hidePanel": "Ocultar panel",
|
||||
"windowTitleBar.showPanel": "Mostrar panel",
|
||||
"tagInput.alreadyAdded": "La etiqueta “{tag}” ya está añadida",
|
||||
"tagInput.remove": "Quitar",
|
||||
"tagInput.createdTag": "Etiqueta “{tag}” creada",
|
||||
"tagInput.createdCategory": "Categoría “{name}” creada",
|
||||
"tagInput.createTag": "Crear etiqueta “{tag}”",
|
||||
"tagInput.createCategory": "Crear categoría “{name}”",
|
||||
"importAnalysis.loadingDefinition": "Cargando definición de importación…",
|
||||
"importAnalysis.namePlaceholder": "Nombre de la definición de importación",
|
||||
"importAnalysis.headerDescription": "Analiza un archivo WXR antes de importar.",
|
||||
"importAnalysis.uploadsFolder": "Carpeta uploads",
|
||||
"importAnalysis.noFolderSelected": "Ninguna carpeta seleccionada",
|
||||
"importAnalysis.wxrFile": "Archivo WXR",
|
||||
"importAnalysis.selectFileToAnalyze": "Selecciona un archivo para analizar",
|
||||
"importAnalysis.analyzing": "Analizando…",
|
||||
"importAnalysis.selectAndAnalyze": "Seleccionar y analizar",
|
||||
"importAnalysis.analyzingWxr": "Analizando archivo WXR…",
|
||||
"importAnalysis.emptyState": "Selecciona un archivo WXR e inicia el análisis.",
|
||||
"importAnalysis.importing": "Importando…",
|
||||
"importAnalysis.importComplete": "Importación completada: {count}",
|
||||
"importAnalysis.importFailed": "La importación falló: {error}",
|
||||
"importAnalysis.untitledImport": "Importación sin título",
|
||||
"importAnalysis.executionStarting": "Iniciando...",
|
||||
"importAnalysis.unknownError": "Error desconocido",
|
||||
"importAnalysis.readyToImport": "Listo para importar:",
|
||||
"importAnalysis.tagsCategories": "etiquetas/categorías",
|
||||
"importAnalysis.posts": "publicaciones",
|
||||
"importAnalysis.media": "medios",
|
||||
"importAnalysis.pages": "páginas",
|
||||
"importAnalysis.nothingToImport": "Nada para importar",
|
||||
"importAnalysis.importItems": "Importar {count} elementos",
|
||||
"importAnalysis.postSlugConflicts": "Conflictos de slug de publicaciones",
|
||||
"importAnalysis.pageSlugConflicts": "Conflictos de slug de páginas",
|
||||
"importAnalysis.postsWithCount": "Publicaciones ({count})",
|
||||
"importAnalysis.otherWithCount": "Otros ({count})",
|
||||
"importAnalysis.pagesWithCount": "Páginas ({count})",
|
||||
"importAnalysis.mediaWithCount": "Medios ({count})",
|
||||
"importAnalysis.site": "Sitio",
|
||||
"importAnalysis.untitled": "Sin título",
|
||||
"importAnalysis.url": "URL",
|
||||
"importAnalysis.language": "Idioma",
|
||||
"importAnalysis.file": "Archivo",
|
||||
"importAnalysis.notAvailable": "N/D",
|
||||
"importAnalysis.new": "nuevo",
|
||||
"importAnalysis.update": "actualización",
|
||||
"importAnalysis.conflict": "conflicto",
|
||||
"importAnalysis.duplicate": "duplicado",
|
||||
"importAnalysis.missing": "faltante",
|
||||
"importAnalysis.categories": "Categorías",
|
||||
"importAnalysis.existing": "existente",
|
||||
"importAnalysis.mapped": "mapeado",
|
||||
"importAnalysis.tags": "Etiquetas",
|
||||
"importAnalysis.dateDistribution": "Distribución por fecha",
|
||||
"importAnalysis.postsPages": "Publicaciones/Páginas",
|
||||
"importAnalysis.total": "total",
|
||||
"importAnalysis.wordpressId": "ID de WordPress",
|
||||
"importAnalysis.type": "Tipo",
|
||||
"importAnalysis.author": "Autor",
|
||||
"importAnalysis.unknown": "Desconocido",
|
||||
"importAnalysis.published": "Publicado",
|
||||
"importAnalysis.excerpt": "Extracto",
|
||||
"importAnalysis.content": "Contenido",
|
||||
"importAnalysis.loading": "Cargando...",
|
||||
"importAnalysis.mimeType": "Tipo MIME",
|
||||
"importAnalysis.uploaded": "Subido",
|
||||
"importAnalysis.parentPostId": "ID de publicación padre",
|
||||
"importAnalysis.description": "Descripción",
|
||||
"importAnalysis.slug": "Slug",
|
||||
"importAnalysis.newEntryWxr": "Nueva entrada (WXR)",
|
||||
"importAnalysis.existingEntry": "Entrada existente",
|
||||
"importAnalysis.resolution": "Resolución",
|
||||
"importAnalysis.ignore": "Ignorar",
|
||||
"importAnalysis.overwrite": "Sobrescribir",
|
||||
"importAnalysis.importNewSlug": "Importar (nuevo slug)",
|
||||
"importAnalysis.status": "Estado",
|
||||
"importAnalysis.title": "Título",
|
||||
"importAnalysis.wpStatus": "Estado WP",
|
||||
"importAnalysis.existingMatch": "Coincidencia existente",
|
||||
"importAnalysis.none": "--",
|
||||
"importAnalysis.filename": "Nombre de archivo",
|
||||
"importAnalysis.path": "Ruta"
|
||||
}
|
||||
|
||||
@@ -556,5 +556,216 @@
|
||||
"panel.error.loadPostLinks": "Impossible de charger les liens d'articles.",
|
||||
"panel.error.loadGitLog": "Impossible de charger le journal Git.",
|
||||
"panel.direction.from": "depuis",
|
||||
"panel.direction.to": "vers"
|
||||
"panel.direction.to": "vers",
|
||||
"settings.editor.description": "Personnalisez le comportement et l’apparence de l’éditeur.",
|
||||
"settings.editor.defaultModeLabel": "Mode par défaut",
|
||||
"settings.editor.defaultModeDescription": "Choisissez le mode d’ouverture de l’éditeur.",
|
||||
"settings.editor.diffViewStyleLabel": "Style de vue diff",
|
||||
"settings.editor.diffViewStyleDescription": "Définissez l’affichage des différences.",
|
||||
"settings.editor.wrapLongLinesLabel": "Retour à la ligne",
|
||||
"settings.editor.wrapLongLinesDescription": "Replier automatiquement les lignes longues.",
|
||||
"settings.editor.wrapLongLinesAria": "Activer le retour à la ligne",
|
||||
"settings.editor.hideUnchangedRegionsLabel": "Masquer les zones inchangées",
|
||||
"settings.editor.hideUnchangedRegionsDescription": "Réduire les sections sans modifications dans la vue diff.",
|
||||
"settings.editor.hideUnchangedRegionsAria": "Activer le masquage des zones inchangées",
|
||||
"settings.content.newCategoryPlaceholder": "Nouvelle catégorie",
|
||||
"settings.content.addCategory": "Ajouter une catégorie",
|
||||
"settings.content.resetDefaults": "Réinitialiser par défaut",
|
||||
"settings.ai.apiKeyLabel": "Clé API",
|
||||
"settings.ai.apiKeyDescription": "Saisissez votre clé API pour activer les fonctionnalités IA.",
|
||||
"settings.ai.apiKeyConfigured": "Clé API configurée",
|
||||
"settings.ai.configured": "Configuré",
|
||||
"settings.ai.changeApiKey": "Changer la clé API",
|
||||
"settings.ai.defaultModelLabel": "Modèle par défaut",
|
||||
"settings.ai.defaultModelDescription": "Sélectionnez le modèle IA utilisé par défaut.",
|
||||
"settings.ai.systemPromptLabel": "Prompt système",
|
||||
"settings.ai.systemPromptDescription": "Définissez les instructions système envoyées au modèle.",
|
||||
"settings.ai.systemPromptPlaceholder": "Écrivez votre prompt système ici…",
|
||||
"settings.ai.savePrompt": "Enregistrer le prompt",
|
||||
"settings.ai.resetPrompt": "Réinitialiser le prompt",
|
||||
"settings.publishing.ftpHostDescription": "Nom d’hôte ou IP du serveur FTP.",
|
||||
"settings.publishing.ftpUsernameDescription": "Nom d’utilisateur FTP.",
|
||||
"settings.publishing.ftpPasswordDescription": "Mot de passe FTP.",
|
||||
"settings.publishing.showPassword": "Afficher le mot de passe",
|
||||
"settings.publishing.hidePassword": "Masquer le mot de passe",
|
||||
"settings.publishing.sshHostDescription": "Nom d’hôte ou IP du serveur SSH.",
|
||||
"settings.publishing.sshUsernameDescription": "Nom d’utilisateur SSH.",
|
||||
"settings.publishing.sshKeyPathDescription": "Chemin vers votre clé privée SSH.",
|
||||
"settings.data.description": "Gérez et réindexez les données locales.",
|
||||
"settings.data.rebuildPostsLabel": "Reconstruire les index des articles",
|
||||
"settings.data.rebuildPostsDescription": "Analyse tous les articles et met à jour l’index de données.",
|
||||
"settings.data.rebuildPostsAction": "Reconstruire",
|
||||
"sidebar.chat.header": "Chat",
|
||||
"sidebar.chat.newChat": "Nouveau chat",
|
||||
"sidebar.chat.apiKeyNeeded": "Une clé API est nécessaire pour utiliser le chat.",
|
||||
"sidebar.chat.noConversations": "Aucune conversation",
|
||||
"sidebar.chat.startNew": "Commencer une nouvelle conversation",
|
||||
"sidebar.chat.deleteConversation": "Supprimer la conversation",
|
||||
"sidebar.chat.createFailed": "Échec de création de la conversation : {error}",
|
||||
"sidebar.chat.deleteFailed": "Échec de suppression de la conversation : {error}",
|
||||
"sidebar.chat.yesterday": "Hier",
|
||||
"sidebar.import.header": "Import",
|
||||
"sidebar.import.newDefinition": "Nouvelle définition",
|
||||
"sidebar.import.none": "Aucune définition d’import",
|
||||
"sidebar.import.createDefinition": "Créer une définition",
|
||||
"sidebar.import.deleteDefinition": "Supprimer la définition",
|
||||
"sidebar.import.createFailed": "Échec de création de la définition : {error}",
|
||||
"sidebar.import.deleteFailed": "Échec de suppression de la définition : {error}",
|
||||
"editor.error.saveTitle": "Échec de l’enregistrement",
|
||||
"editor.error.saveMessage": "Impossible d’enregistrer l’article : {error}",
|
||||
"editor.error.publishTitle": "Échec de la publication",
|
||||
"editor.error.publishMessage": "Impossible de publier l’article : {error}",
|
||||
"editor.error.discardTitle": "Échec de l’abandon",
|
||||
"editor.error.deleteTitle": "Échec de la suppression",
|
||||
"editor.error.operationMessage": "L’opération a échoué : {error}",
|
||||
"editor.error.deletePostMessage": "Impossible de supprimer l’article : {error}",
|
||||
"editor.error.fetchPostReferencesMessage": "Impossible de récupérer les références d’article : {error}",
|
||||
"editor.confirm.discardChanges": "Ignorer les modifications non enregistrées ?",
|
||||
"editor.confirm.deleteDraft": "Supprimer ce brouillon ?",
|
||||
"editor.toast.published": "Article publié",
|
||||
"editor.toast.reverted": "Modifications annulées",
|
||||
"editor.toast.draftDeleted": "Brouillon supprimé",
|
||||
"editor.toast.postDeleted": "Article supprimé",
|
||||
"editor.media.notFound": "Média introuvable",
|
||||
"editor.media.error.analyzeImage": "Impossible d’analyser l’image : {error}",
|
||||
"editor.media.error.updateTitle": "Échec de mise à jour",
|
||||
"editor.media.error.updateMessage": "Impossible de mettre à jour le média : {error}",
|
||||
"editor.media.error.replaceTitle": "Échec du remplacement",
|
||||
"editor.media.error.replaceMessage": "Impossible de remplacer le fichier média : {error}",
|
||||
"editor.media.error.deleteMessage": "Impossible de supprimer le média : {error}",
|
||||
"editor.media.error.fetchReferencesMessage": "Impossible de récupérer les références du média : {error}",
|
||||
"editor.media.toast.aiApplied": "Suggestions IA appliquées",
|
||||
"editor.media.toast.linkedToPost": "Média lié à l’article",
|
||||
"editor.media.toast.linkFailed": "Échec de liaison du média : {error}",
|
||||
"editor.media.toast.unlinkedFromPost": "Média dissocié de l’article",
|
||||
"editor.media.toast.unlinkFailed": "Échec de dissociation du média : {error}",
|
||||
"editor.media.toast.updated": "Média mis à jour",
|
||||
"editor.media.toast.fileReplaced": "Fichier média remplacé",
|
||||
"editor.media.toast.deleted": "Média supprimé",
|
||||
"editor.media.quickActions.title": "Actions rapides",
|
||||
"editor.media.quickActions.analyzing": "🔎 Analyse en cours…",
|
||||
"editor.media.quickActions.button": "✨ Analyser avec l’IA",
|
||||
"editor.media.quickActions.aiTitle": "Titre suggéré par l’IA",
|
||||
"editor.media.quickActions.aiDescription": "Générez automatiquement un titre, un texte alternatif et une légende.",
|
||||
"editor.media.replaceFile": "Remplacer le fichier",
|
||||
"editor.media.field.fileName": "Nom du fichier",
|
||||
"editor.media.field.type": "Type",
|
||||
"editor.media.field.size": "Taille",
|
||||
"editor.media.field.dimensions": "Dimensions",
|
||||
"editor.media.field.title": "Titre",
|
||||
"editor.media.field.altText": "Texte alternatif",
|
||||
"editor.media.field.caption": "Légende",
|
||||
"editor.media.field.tags": "Tags",
|
||||
"editor.media.field.author": "Auteur",
|
||||
"editor.media.placeholder.title": "Saisissez un titre",
|
||||
"editor.media.placeholder.altText": "Décrivez l’image pour l’accessibilité",
|
||||
"editor.media.placeholder.caption": "Ajouter une légende",
|
||||
"editor.media.placeholder.tags": "Ajouter des tags",
|
||||
"editor.media.placeholder.author": "Nom de l’auteur",
|
||||
"editor.media.linkedPosts": "Articles liés",
|
||||
"editor.media.linkToPostTitle": "Lier à un article",
|
||||
"editor.media.linkAction": "Lier",
|
||||
"editor.media.searchPosts": "Rechercher des articles",
|
||||
"editor.media.noMatchingPosts": "Aucun article correspondant à « {query} »",
|
||||
"editor.media.noPostsToLink": "Aucun article disponible à lier",
|
||||
"editor.media.morePosts": "{count} autres articles",
|
||||
"editor.media.notLinked": "Non lié",
|
||||
"editor.media.openPost": "Ouvrir l’article",
|
||||
"editor.media.unlinkFromPost": "Dissocier de l’article",
|
||||
"postSearch.placeholder": "Rechercher des articles…",
|
||||
"postSearch.searching": "Recherche…",
|
||||
"postSearch.typeMore": "Tapez plus de caractères pour rechercher",
|
||||
"postSearch.noResults": "Aucun résultat pour « {query} »",
|
||||
"postSearch.hint": "Rechercher par titre, slug ou contenu",
|
||||
"statusBar.posts": "Articles",
|
||||
"statusBar.media": "Médias",
|
||||
"statusBar.theme": "Thème : {theme}",
|
||||
"statusBar.ui": "UI",
|
||||
"statusBar.uiLanguage": "Langue de l’interface",
|
||||
"windowTitleBar.toggleSidebar": "Basculer la barre latérale",
|
||||
"windowTitleBar.hideSidebar": "Masquer la barre latérale",
|
||||
"windowTitleBar.showSidebar": "Afficher la barre latérale",
|
||||
"windowTitleBar.togglePanel": "Basculer le panneau",
|
||||
"windowTitleBar.hidePanel": "Masquer le panneau",
|
||||
"windowTitleBar.showPanel": "Afficher le panneau",
|
||||
"tagInput.alreadyAdded": "Le tag « {tag} » est déjà ajouté",
|
||||
"tagInput.remove": "Supprimer",
|
||||
"tagInput.createdTag": "Tag « {tag} » créé",
|
||||
"tagInput.createdCategory": "Catégorie « {name} » créée",
|
||||
"tagInput.createTag": "Créer le tag « {tag} »",
|
||||
"tagInput.createCategory": "Créer la catégorie « {name} »",
|
||||
"importAnalysis.loadingDefinition": "Chargement de la définition d’import…",
|
||||
"importAnalysis.namePlaceholder": "Nom de la définition d’import",
|
||||
"importAnalysis.headerDescription": "Analysez un fichier WXR avant import.",
|
||||
"importAnalysis.uploadsFolder": "Dossier d’uploads",
|
||||
"importAnalysis.noFolderSelected": "Aucun dossier sélectionné",
|
||||
"importAnalysis.wxrFile": "Fichier WXR",
|
||||
"importAnalysis.selectFileToAnalyze": "Sélectionnez un fichier à analyser",
|
||||
"importAnalysis.analyzing": "Analyse…",
|
||||
"importAnalysis.selectAndAnalyze": "Sélectionner et analyser",
|
||||
"importAnalysis.analyzingWxr": "Analyse du fichier WXR…",
|
||||
"importAnalysis.emptyState": "Sélectionnez un fichier WXR et lancez l’analyse.",
|
||||
"importAnalysis.importing": "Import en cours…",
|
||||
"importAnalysis.importComplete": "Import terminé : {count}",
|
||||
"importAnalysis.importFailed": "Échec de l’import : {error}",
|
||||
"importAnalysis.untitledImport": "Import sans titre",
|
||||
"importAnalysis.executionStarting": "Démarrage...",
|
||||
"importAnalysis.unknownError": "Erreur inconnue",
|
||||
"importAnalysis.readyToImport": "Prêt à importer :",
|
||||
"importAnalysis.tagsCategories": "tags/catégories",
|
||||
"importAnalysis.posts": "articles",
|
||||
"importAnalysis.media": "médias",
|
||||
"importAnalysis.pages": "pages",
|
||||
"importAnalysis.nothingToImport": "Rien à importer",
|
||||
"importAnalysis.importItems": "Importer {count} éléments",
|
||||
"importAnalysis.postSlugConflicts": "Conflits de slug d’article",
|
||||
"importAnalysis.pageSlugConflicts": "Conflits de slug de page",
|
||||
"importAnalysis.postsWithCount": "Articles ({count})",
|
||||
"importAnalysis.otherWithCount": "Autres ({count})",
|
||||
"importAnalysis.pagesWithCount": "Pages ({count})",
|
||||
"importAnalysis.mediaWithCount": "Médias ({count})",
|
||||
"importAnalysis.site": "Site",
|
||||
"importAnalysis.untitled": "Sans titre",
|
||||
"importAnalysis.url": "URL",
|
||||
"importAnalysis.language": "Langue",
|
||||
"importAnalysis.file": "Fichier",
|
||||
"importAnalysis.notAvailable": "N/D",
|
||||
"importAnalysis.new": "nouveau",
|
||||
"importAnalysis.update": "mise à jour",
|
||||
"importAnalysis.conflict": "conflit",
|
||||
"importAnalysis.duplicate": "doublon",
|
||||
"importAnalysis.missing": "manquant",
|
||||
"importAnalysis.categories": "Catégories",
|
||||
"importAnalysis.existing": "existant",
|
||||
"importAnalysis.mapped": "mappé",
|
||||
"importAnalysis.tags": "Tags",
|
||||
"importAnalysis.dateDistribution": "Répartition par date",
|
||||
"importAnalysis.postsPages": "Articles/Pages",
|
||||
"importAnalysis.total": "total",
|
||||
"importAnalysis.wordpressId": "ID WordPress",
|
||||
"importAnalysis.type": "Type",
|
||||
"importAnalysis.author": "Auteur",
|
||||
"importAnalysis.unknown": "Inconnu",
|
||||
"importAnalysis.published": "Publié",
|
||||
"importAnalysis.excerpt": "Extrait",
|
||||
"importAnalysis.content": "Contenu",
|
||||
"importAnalysis.loading": "Chargement...",
|
||||
"importAnalysis.mimeType": "Type MIME",
|
||||
"importAnalysis.uploaded": "Téléversé",
|
||||
"importAnalysis.parentPostId": "ID du post parent",
|
||||
"importAnalysis.description": "Description",
|
||||
"importAnalysis.slug": "Slug",
|
||||
"importAnalysis.newEntryWxr": "Nouvelle entrée (WXR)",
|
||||
"importAnalysis.existingEntry": "Entrée existante",
|
||||
"importAnalysis.resolution": "Résolution",
|
||||
"importAnalysis.ignore": "Ignorer",
|
||||
"importAnalysis.overwrite": "Écraser",
|
||||
"importAnalysis.importNewSlug": "Importer (nouveau slug)",
|
||||
"importAnalysis.status": "Statut",
|
||||
"importAnalysis.title": "Titre",
|
||||
"importAnalysis.wpStatus": "Statut WP",
|
||||
"importAnalysis.existingMatch": "Correspondance existante",
|
||||
"importAnalysis.none": "--",
|
||||
"importAnalysis.filename": "Nom de fichier",
|
||||
"importAnalysis.path": "Chemin"
|
||||
}
|
||||
|
||||
@@ -556,5 +556,216 @@
|
||||
"panel.error.loadPostLinks": "Impossibile caricare i collegamenti del post.",
|
||||
"panel.error.loadGitLog": "Impossibile caricare il registro Git.",
|
||||
"panel.direction.from": "da",
|
||||
"panel.direction.to": "a"
|
||||
"panel.direction.to": "a",
|
||||
"settings.editor.description": "Personalizza comportamento e aspetto dell’editor.",
|
||||
"settings.editor.defaultModeLabel": "Modalità predefinita",
|
||||
"settings.editor.defaultModeDescription": "Scegli la modalità di apertura dell’editor.",
|
||||
"settings.editor.diffViewStyleLabel": "Stile vista diff",
|
||||
"settings.editor.diffViewStyleDescription": "Definisci come visualizzare le differenze.",
|
||||
"settings.editor.wrapLongLinesLabel": "A capo automatico",
|
||||
"settings.editor.wrapLongLinesDescription": "Vai a capo automaticamente per le righe lunghe.",
|
||||
"settings.editor.wrapLongLinesAria": "Attiva a capo automatico",
|
||||
"settings.editor.hideUnchangedRegionsLabel": "Nascondi aree non modificate",
|
||||
"settings.editor.hideUnchangedRegionsDescription": "Comprimi le sezioni senza modifiche nella vista diff.",
|
||||
"settings.editor.hideUnchangedRegionsAria": "Attiva nascondi aree non modificate",
|
||||
"settings.content.newCategoryPlaceholder": "Nuova categoria",
|
||||
"settings.content.addCategory": "Aggiungi categoria",
|
||||
"settings.content.resetDefaults": "Ripristina predefiniti",
|
||||
"settings.ai.apiKeyLabel": "Chiave API",
|
||||
"settings.ai.apiKeyDescription": "Inserisci la tua chiave API per abilitare le funzioni IA.",
|
||||
"settings.ai.apiKeyConfigured": "Chiave API configurata",
|
||||
"settings.ai.configured": "Configurato",
|
||||
"settings.ai.changeApiKey": "Cambia chiave API",
|
||||
"settings.ai.defaultModelLabel": "Modello predefinito",
|
||||
"settings.ai.defaultModelDescription": "Seleziona il modello IA usato per default.",
|
||||
"settings.ai.systemPromptLabel": "Prompt di sistema",
|
||||
"settings.ai.systemPromptDescription": "Definisci le istruzioni di sistema inviate al modello.",
|
||||
"settings.ai.systemPromptPlaceholder": "Scrivi qui il tuo prompt di sistema…",
|
||||
"settings.ai.savePrompt": "Salva prompt",
|
||||
"settings.ai.resetPrompt": "Reimposta prompt",
|
||||
"settings.publishing.ftpHostDescription": "Hostname o IP del server FTP.",
|
||||
"settings.publishing.ftpUsernameDescription": "Nome utente FTP.",
|
||||
"settings.publishing.ftpPasswordDescription": "Password FTP.",
|
||||
"settings.publishing.showPassword": "Mostra password",
|
||||
"settings.publishing.hidePassword": "Nascondi password",
|
||||
"settings.publishing.sshHostDescription": "Hostname o IP del server SSH.",
|
||||
"settings.publishing.sshUsernameDescription": "Nome utente SSH.",
|
||||
"settings.publishing.sshKeyPathDescription": "Percorso della tua chiave privata SSH.",
|
||||
"settings.data.description": "Gestisci e ricostruisci i dati locali.",
|
||||
"settings.data.rebuildPostsLabel": "Ricostruisci indice articoli",
|
||||
"settings.data.rebuildPostsDescription": "Analizza tutti gli articoli e aggiorna l’indice dati.",
|
||||
"settings.data.rebuildPostsAction": "Ricostruisci",
|
||||
"sidebar.chat.header": "Chat",
|
||||
"sidebar.chat.newChat": "Nuova chat",
|
||||
"sidebar.chat.apiKeyNeeded": "Serve una chiave API per usare la chat.",
|
||||
"sidebar.chat.noConversations": "Nessuna conversazione",
|
||||
"sidebar.chat.startNew": "Inizia una nuova conversazione",
|
||||
"sidebar.chat.deleteConversation": "Elimina conversazione",
|
||||
"sidebar.chat.createFailed": "Creazione conversazione non riuscita: {error}",
|
||||
"sidebar.chat.deleteFailed": "Eliminazione conversazione non riuscita: {error}",
|
||||
"sidebar.chat.yesterday": "Ieri",
|
||||
"sidebar.import.header": "Importazione",
|
||||
"sidebar.import.newDefinition": "Nuova definizione",
|
||||
"sidebar.import.none": "Nessuna definizione di importazione",
|
||||
"sidebar.import.createDefinition": "Crea definizione",
|
||||
"sidebar.import.deleteDefinition": "Elimina definizione",
|
||||
"sidebar.import.createFailed": "Creazione definizione non riuscita: {error}",
|
||||
"sidebar.import.deleteFailed": "Eliminazione definizione non riuscita: {error}",
|
||||
"editor.error.saveTitle": "Salvataggio non riuscito",
|
||||
"editor.error.saveMessage": "Impossibile salvare l’articolo: {error}",
|
||||
"editor.error.publishTitle": "Pubblicazione non riuscita",
|
||||
"editor.error.publishMessage": "Impossibile pubblicare l’articolo: {error}",
|
||||
"editor.error.discardTitle": "Annullamento non riuscito",
|
||||
"editor.error.deleteTitle": "Eliminazione non riuscita",
|
||||
"editor.error.operationMessage": "Operazione non riuscita: {error}",
|
||||
"editor.error.deletePostMessage": "Impossibile eliminare l’articolo: {error}",
|
||||
"editor.error.fetchPostReferencesMessage": "Impossibile recuperare i riferimenti dell’articolo: {error}",
|
||||
"editor.confirm.discardChanges": "Scartare le modifiche non salvate?",
|
||||
"editor.confirm.deleteDraft": "Eliminare questa bozza?",
|
||||
"editor.toast.published": "Articolo pubblicato",
|
||||
"editor.toast.reverted": "Modifiche annullate",
|
||||
"editor.toast.draftDeleted": "Bozza eliminata",
|
||||
"editor.toast.postDeleted": "Articolo eliminato",
|
||||
"editor.media.notFound": "Media non trovato",
|
||||
"editor.media.error.analyzeImage": "Impossibile analizzare l’immagine: {error}",
|
||||
"editor.media.error.updateTitle": "Aggiornamento non riuscito",
|
||||
"editor.media.error.updateMessage": "Impossibile aggiornare il media: {error}",
|
||||
"editor.media.error.replaceTitle": "Sostituzione non riuscita",
|
||||
"editor.media.error.replaceMessage": "Impossibile sostituire il file media: {error}",
|
||||
"editor.media.error.deleteMessage": "Impossibile eliminare il media: {error}",
|
||||
"editor.media.error.fetchReferencesMessage": "Impossibile recuperare i riferimenti del media: {error}",
|
||||
"editor.media.toast.aiApplied": "Suggerimenti IA applicati",
|
||||
"editor.media.toast.linkedToPost": "Media collegato all’articolo",
|
||||
"editor.media.toast.linkFailed": "Collegamento del media non riuscito: {error}",
|
||||
"editor.media.toast.unlinkedFromPost": "Media scollegato dall’articolo",
|
||||
"editor.media.toast.unlinkFailed": "Scollegamento del media non riuscito: {error}",
|
||||
"editor.media.toast.updated": "Media aggiornato",
|
||||
"editor.media.toast.fileReplaced": "File media sostituito",
|
||||
"editor.media.toast.deleted": "Media eliminato",
|
||||
"editor.media.quickActions.title": "Azioni rapide",
|
||||
"editor.media.quickActions.analyzing": "🔎 Analisi in corso…",
|
||||
"editor.media.quickActions.button": "✨ Analizza con IA",
|
||||
"editor.media.quickActions.aiTitle": "Titolo suggerito dall’IA",
|
||||
"editor.media.quickActions.aiDescription": "Genera automaticamente titolo, testo alternativo e didascalia.",
|
||||
"editor.media.replaceFile": "Sostituisci file",
|
||||
"editor.media.field.fileName": "Nome file",
|
||||
"editor.media.field.type": "Tipo",
|
||||
"editor.media.field.size": "Dimensione",
|
||||
"editor.media.field.dimensions": "Dimensioni",
|
||||
"editor.media.field.title": "Titolo",
|
||||
"editor.media.field.altText": "Testo alternativo",
|
||||
"editor.media.field.caption": "Didascalia",
|
||||
"editor.media.field.tags": "Tag",
|
||||
"editor.media.field.author": "Autore",
|
||||
"editor.media.placeholder.title": "Inserisci un titolo",
|
||||
"editor.media.placeholder.altText": "Descrivi l’immagine per l’accessibilità",
|
||||
"editor.media.placeholder.caption": "Aggiungi una didascalia",
|
||||
"editor.media.placeholder.tags": "Aggiungi tag",
|
||||
"editor.media.placeholder.author": "Nome autore",
|
||||
"editor.media.linkedPosts": "Articoli collegati",
|
||||
"editor.media.linkToPostTitle": "Collega a un articolo",
|
||||
"editor.media.linkAction": "Collega",
|
||||
"editor.media.searchPosts": "Cerca articoli",
|
||||
"editor.media.noMatchingPosts": "Nessun articolo corrisponde a “{query}”",
|
||||
"editor.media.noPostsToLink": "Nessun articolo disponibile da collegare",
|
||||
"editor.media.morePosts": "{count} altri articoli",
|
||||
"editor.media.notLinked": "Non collegato",
|
||||
"editor.media.openPost": "Apri articolo",
|
||||
"editor.media.unlinkFromPost": "Scollega dall’articolo",
|
||||
"postSearch.placeholder": "Cerca articoli…",
|
||||
"postSearch.searching": "Ricerca in corso…",
|
||||
"postSearch.typeMore": "Digita più caratteri per cercare",
|
||||
"postSearch.noResults": "Nessun risultato per “{query}”",
|
||||
"postSearch.hint": "Cerca per titolo, slug o contenuto",
|
||||
"statusBar.posts": "Articoli",
|
||||
"statusBar.media": "Media",
|
||||
"statusBar.theme": "Tema: {theme}",
|
||||
"statusBar.ui": "UI",
|
||||
"statusBar.uiLanguage": "Lingua interfaccia",
|
||||
"windowTitleBar.toggleSidebar": "Mostra/Nascondi barra laterale",
|
||||
"windowTitleBar.hideSidebar": "Nascondi barra laterale",
|
||||
"windowTitleBar.showSidebar": "Mostra barra laterale",
|
||||
"windowTitleBar.togglePanel": "Mostra/Nascondi pannello",
|
||||
"windowTitleBar.hidePanel": "Nascondi pannello",
|
||||
"windowTitleBar.showPanel": "Mostra pannello",
|
||||
"tagInput.alreadyAdded": "Il tag “{tag}” è già stato aggiunto",
|
||||
"tagInput.remove": "Rimuovi",
|
||||
"tagInput.createdTag": "Tag “{tag}” creato",
|
||||
"tagInput.createdCategory": "Categoria “{name}” creata",
|
||||
"tagInput.createTag": "Crea tag “{tag}”",
|
||||
"tagInput.createCategory": "Crea categoria “{name}”",
|
||||
"importAnalysis.loadingDefinition": "Caricamento definizione di importazione…",
|
||||
"importAnalysis.namePlaceholder": "Nome definizione di importazione",
|
||||
"importAnalysis.headerDescription": "Analizza un file WXR prima dell’importazione.",
|
||||
"importAnalysis.uploadsFolder": "Cartella uploads",
|
||||
"importAnalysis.noFolderSelected": "Nessuna cartella selezionata",
|
||||
"importAnalysis.wxrFile": "File WXR",
|
||||
"importAnalysis.selectFileToAnalyze": "Seleziona un file da analizzare",
|
||||
"importAnalysis.analyzing": "Analisi…",
|
||||
"importAnalysis.selectAndAnalyze": "Seleziona e analizza",
|
||||
"importAnalysis.analyzingWxr": "Analisi del file WXR…",
|
||||
"importAnalysis.emptyState": "Seleziona un file WXR e avvia l’analisi.",
|
||||
"importAnalysis.importing": "Importazione in corso…",
|
||||
"importAnalysis.importComplete": "Importazione completata: {count}",
|
||||
"importAnalysis.importFailed": "Importazione non riuscita: {error}",
|
||||
"importAnalysis.untitledImport": "Importazione senza titolo",
|
||||
"importAnalysis.executionStarting": "Avvio...",
|
||||
"importAnalysis.unknownError": "Errore sconosciuto",
|
||||
"importAnalysis.readyToImport": "Pronto per importare:",
|
||||
"importAnalysis.tagsCategories": "tag/categorie",
|
||||
"importAnalysis.posts": "articoli",
|
||||
"importAnalysis.media": "media",
|
||||
"importAnalysis.pages": "pagine",
|
||||
"importAnalysis.nothingToImport": "Niente da importare",
|
||||
"importAnalysis.importItems": "Importa {count} elementi",
|
||||
"importAnalysis.postSlugConflicts": "Conflitti slug articoli",
|
||||
"importAnalysis.pageSlugConflicts": "Conflitti slug pagine",
|
||||
"importAnalysis.postsWithCount": "Articoli ({count})",
|
||||
"importAnalysis.otherWithCount": "Altro ({count})",
|
||||
"importAnalysis.pagesWithCount": "Pagine ({count})",
|
||||
"importAnalysis.mediaWithCount": "Media ({count})",
|
||||
"importAnalysis.site": "Sito",
|
||||
"importAnalysis.untitled": "Senza titolo",
|
||||
"importAnalysis.url": "URL",
|
||||
"importAnalysis.language": "Lingua",
|
||||
"importAnalysis.file": "File",
|
||||
"importAnalysis.notAvailable": "N/D",
|
||||
"importAnalysis.new": "nuovo",
|
||||
"importAnalysis.update": "aggiornamento",
|
||||
"importAnalysis.conflict": "conflitto",
|
||||
"importAnalysis.duplicate": "duplicato",
|
||||
"importAnalysis.missing": "mancante",
|
||||
"importAnalysis.categories": "Categorie",
|
||||
"importAnalysis.existing": "esistente",
|
||||
"importAnalysis.mapped": "mappato",
|
||||
"importAnalysis.tags": "Tag",
|
||||
"importAnalysis.dateDistribution": "Distribuzione per data",
|
||||
"importAnalysis.postsPages": "Articoli/Pagine",
|
||||
"importAnalysis.total": "totale",
|
||||
"importAnalysis.wordpressId": "ID WordPress",
|
||||
"importAnalysis.type": "Tipo",
|
||||
"importAnalysis.author": "Autore",
|
||||
"importAnalysis.unknown": "Sconosciuto",
|
||||
"importAnalysis.published": "Pubblicato",
|
||||
"importAnalysis.excerpt": "Estratto",
|
||||
"importAnalysis.content": "Contenuto",
|
||||
"importAnalysis.loading": "Caricamento...",
|
||||
"importAnalysis.mimeType": "Tipo MIME",
|
||||
"importAnalysis.uploaded": "Caricato",
|
||||
"importAnalysis.parentPostId": "ID articolo padre",
|
||||
"importAnalysis.description": "Descrizione",
|
||||
"importAnalysis.slug": "Slug",
|
||||
"importAnalysis.newEntryWxr": "Nuova voce (WXR)",
|
||||
"importAnalysis.existingEntry": "Voce esistente",
|
||||
"importAnalysis.resolution": "Risoluzione",
|
||||
"importAnalysis.ignore": "Ignora",
|
||||
"importAnalysis.overwrite": "Sovrascrivi",
|
||||
"importAnalysis.importNewSlug": "Importa (nuovo slug)",
|
||||
"importAnalysis.status": "Stato",
|
||||
"importAnalysis.title": "Titolo",
|
||||
"importAnalysis.wpStatus": "Stato WP",
|
||||
"importAnalysis.existingMatch": "Corrispondenza esistente",
|
||||
"importAnalysis.none": "--",
|
||||
"importAnalysis.filename": "Nome file",
|
||||
"importAnalysis.path": "Percorso"
|
||||
}
|
||||
|
||||
56
tests/renderer/hardcodedUiLiteralsPhase1.test.ts
Normal file
56
tests/renderer/hardcodedUiLiteralsPhase1.test.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const ROOT = process.cwd();
|
||||
|
||||
function read(relativePath: string): string {
|
||||
return readFileSync(join(ROOT, relativePath), 'utf8');
|
||||
}
|
||||
|
||||
describe('Phase 1 i18n hardcoded literals', () => {
|
||||
it('does not keep known hardcoded user-facing literals in renderer components', () => {
|
||||
const checks: Array<{ file: string; literals: string[] }> = [
|
||||
{
|
||||
file: 'src/renderer/components/PostSearchModal/PostSearchModal.tsx',
|
||||
literals: [
|
||||
'Search posts by title or content...',
|
||||
'Searching...',
|
||||
'Type at least 2 characters to search',
|
||||
'Use ↑↓ to navigate, Enter to select, Esc to close',
|
||||
],
|
||||
},
|
||||
{
|
||||
file: 'src/renderer/components/StatusBar/StatusBar.tsx',
|
||||
literals: ['<span>{totalPosts} posts</span>', '<span>{media.length} media</span>', 'Theme: {activeTheme}', 'aria-label="UI language"'],
|
||||
},
|
||||
{
|
||||
file: 'src/renderer/components/WindowTitleBar/WindowTitleBar.tsx',
|
||||
literals: ['Toggle Sidebar', 'Toggle Panel'],
|
||||
},
|
||||
{
|
||||
file: 'src/renderer/components/Sidebar/Sidebar.tsx',
|
||||
literals: ['AI ASSISTANT', 'IMPORTS', 'No conversations yet', 'Create an import definition'],
|
||||
},
|
||||
{
|
||||
file: 'src/renderer/components/TagInput/TagInput.tsx',
|
||||
literals: ['Tag already added', 'Create {mode === \'category\' ? \'category\' : \'tag\'} "{inputValue.trim()}"'],
|
||||
},
|
||||
{
|
||||
file: 'src/renderer/components/Editor/Editor.tsx',
|
||||
literals: ['Save Failed', 'Post published', 'Media not found', 'title="Quick Actions"'],
|
||||
},
|
||||
{
|
||||
file: 'src/renderer/components/ImportAnalysisView/ImportAnalysisView.tsx',
|
||||
literals: ['Loading import definition...', 'Import name...', 'Select & Analyze', 'Import completed successfully!'],
|
||||
},
|
||||
];
|
||||
|
||||
for (const check of checks) {
|
||||
const source = read(check.file);
|
||||
for (const literal of check.literals) {
|
||||
expect(source).not.toContain(literal);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user