update apply view local lang

This commit is contained in:
duanfuxiang 2025-05-01 15:42:10 +08:00
parent 2f824134b6
commit cc8cc26e0b
4 changed files with 248 additions and 180 deletions

View File

@ -5,11 +5,9 @@ import ContentEditable from 'react-contenteditable'
import { ApplyViewState } from '../../ApplyView' import { ApplyViewState } from '../../ApplyView'
import { useApp } from '../../contexts/AppContext' import { useApp } from '../../contexts/AppContext'
import { t } from '../../lang/helpers'
export default function ApplyViewRoot({ export default function ApplyViewRoot({ state, close }: {
state,
close,
}: {
state: ApplyViewState state: ApplyViewState
close: () => void close: () => void
}) { }) {
@ -29,14 +27,14 @@ export default function ApplyViewRoot({
// Track which lines have been accepted or excluded // Track which lines have been accepted or excluded
const [diffStatus, setDiffStatus] = useState<Array<'active' | 'accepted' | 'excluded'>>([]) const [diffStatus, setDiffStatus] = useState<Array<'active' | 'accepted' | 'excluded'>>([])
const [diff] = useState<Change[]>(() => { const [diff] = useState<Change[]>(() => {
const initialDiff = diffLines(state.oldContent, state.newContent) const initialDiff = diffLines(state.oldContent, state.newContent)
// Initialize all lines as 'active' // Initialize all lines as 'active'
setDiffStatus(initialDiff.map(() => 'active')) setDiffStatus(initialDiff.map(() => 'active'))
return initialDiff return initialDiff
}) })
// Store edited content for each diff part // Store edited content for each diff part
const [editedContents, setEditedContents] = useState<string[]>( const [editedContents, setEditedContents] = useState<string[]>(
diff.map(part => part.value) diff.map(part => part.value)
@ -46,170 +44,170 @@ export default function ApplyViewRoot({
// Filter and process content based on diffStatus // Filter and process content based on diffStatus
const newContent = diff.reduce((result, change, index) => { const newContent = diff.reduce((result, change, index) => {
// Keep unchanged content, non-excluded additions, or excluded removals // Keep unchanged content, non-excluded additions, or excluded removals
if ((!change.added && !change.removed) || if ((!change.added && !change.removed) ||
(change.added && diffStatus[index] !== 'excluded') || (change.added && diffStatus[index] !== 'excluded') ||
(change.removed && diffStatus[index] === 'excluded')) { (change.removed && diffStatus[index] === 'excluded')) {
return result + editedContents[index]; return result + editedContents[index];
} }
return result; return result;
}, '') }, '')
const file = app.vault.getFileByPath(state.file) const file = app.vault.getFileByPath(state.file)
if (!file) { if (!file) {
throw new Error('File not found') throw new Error(String(t('applyView.fileNotFound')))
} }
await app.vault.modify(file, newContent) await app.vault.modify(file, newContent)
if (state.onClose) { if (state.onClose) {
state.onClose(true) state.onClose(true)
} }
close() close()
} }
const handleReject = async () => { const handleReject = async () => {
if (state.onClose) { if (state.onClose) {
state.onClose(false) state.onClose(false)
} }
close() close()
} }
const excludeDiffLine = (index: number) => { const excludeDiffLine = (index: number) => {
setDiffStatus(prevStatus => { setDiffStatus(prevStatus => {
const newStatus = [...prevStatus] const newStatus = [...prevStatus]
// Mark line as excluded // Mark line as excluded
newStatus[index] = 'excluded' newStatus[index] = 'excluded'
return newStatus return newStatus
}) })
} }
const acceptDiffLine = (index: number) => { const acceptDiffLine = (index: number) => {
setDiffStatus(prevStatus => { setDiffStatus(prevStatus => {
const newStatus = [...prevStatus] const newStatus = [...prevStatus]
// Mark line as accepted // Mark line as accepted
newStatus[index] = 'accepted' newStatus[index] = 'accepted'
return newStatus return newStatus
}) })
} }
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyDown = (event: KeyboardEvent) => {
const modifierKey = Platform.isMacOS ? event.metaKey : event.ctrlKey; const modifierKey = Platform.isMacOS ? event.metaKey : event.ctrlKey;
if (modifierKey) { if (modifierKey) {
if (event.key === 'Enter') { if (event.key === 'Enter') {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
handleAccept(); handleAccept();
} else if (event.key === 'Backspace') { } else if (event.key === 'Backspace') {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
handleReject(); handleReject();
} }
} }
} }
// Handle content editing changes
const handleContentChange = (index: number, evt: { target: { value: string } }) => {
const newEditedContents = [...editedContents];
newEditedContents[index] = evt.target.value;
setEditedContents(newEditedContents);
}
// Add event listeners on mount and remove on unmount // Handle content editing changes
useEffect(() => { const handleContentChange = (index: number, evt: { target: { value: string } }) => {
const handler = (e: KeyboardEvent) => handleKeyDown(e); const newEditedContents = [...editedContents];
window.addEventListener('keydown', handler, true); newEditedContents[index] = evt.target.value;
return () => { setEditedContents(newEditedContents);
window.removeEventListener('keydown', handler, true); }
}
}, [handleAccept, handleReject]) // Dependencies for the effect
return ( // Add event listeners on mount and remove on unmount
<div id="infio-apply-view"> useEffect(() => {
<div className="view-header"> const handler = (e: KeyboardEvent) => handleKeyDown(e);
<div className="view-header-left"> window.addEventListener('keydown', handler, true);
<div className="view-header-nav-buttons"></div> return () => {
</div> window.removeEventListener('keydown', handler, true);
<div className="view-header-title-container mod-at-start"> }
<div className="view-header-title"> }, [handleAccept, handleReject]) // Dependencies for the effect
Applying: {state?.file ?? ''}
</div>
<div className="view-actions">
<button
className="clickable-icon view-action infio-approve-button"
aria-label="Accept changes"
onClick={handleAccept}
>
{acceptIcon && '✓'}
Accept All {getShortcutText('accept')}
</button>
<button
className="clickable-icon view-action infio-reject-button"
aria-label="Reject changes"
onClick={handleReject}
>
{rejectIcon && '✗'}
Reject All {getShortcutText('reject')}
</button>
</div>
</div>
</div>
<div className="view-content"> return (
<div className="markdown-source-view cm-s-obsidian mod-cm6 node-insert-event is-readable-line-width is-live-preview is-folding show-properties"> <div id="infio-apply-view">
<div className="cm-editor"> <div className="view-header">
<div className="cm-scroller"> <div className="view-header-left">
<div className="cm-sizer"> <div className="view-header-nav-buttons"></div>
<div className="infio-inline-title"> </div>
{state?.file <div className="view-header-title-container mod-at-start">
? state.file.replace(/\.[^/.]+$/, '') <div className="view-header-title">
: ''} {t('applyView.applyingFile').replace('{{file}}', state?.file ?? '')}
</div> </div>
<div className="view-actions">
<button
className="clickable-icon view-action infio-approve-button"
aria-label={t('applyView.acceptChanges')}
onClick={handleAccept}
>
{acceptIcon && '✓'}
{t('applyView.acceptAll').replace('{{shortcut}}', getShortcutText('accept'))}
</button>
<button
className="clickable-icon view-action infio-reject-button"
aria-label={t('applyView.rejectChanges')}
onClick={handleReject}
>
{rejectIcon && '✗'}
{t('applyView.rejectAll').replace('{{shortcut}}', getShortcutText('reject'))}
</button>
</div>
</div>
</div>
{diff.map((part, index) => { <div className="view-content">
// Determine line display status based on diffStatus <div className="markdown-source-view cm-s-obsidian mod-cm6 node-insert-event is-readable-line-width is-live-preview is-folding show-properties">
const status = diffStatus[index] <div className="cm-editor">
const isHidden = <div className="cm-scroller">
(part.added && status === 'excluded') || <div className="cm-sizer">
(part.removed && status === 'accepted') <div className="infio-inline-title">
{state?.file
if (isHidden) return null ? state.file.replace(/\.[^/.]+$/, '')
: ''}
</div>
return ( {diff.map((part, index) => {
<div // Determine line display status based on diffStatus
key={index} const status = diffStatus[index]
className={`infio-diff-line ${part.added ? 'added' : part.removed ? 'removed' : ''} ${status !== 'active' ? status : ''}`} const isHidden =
> (part.added && status === 'excluded') ||
<div className="infio-diff-content-wrapper"> (part.removed && status === 'accepted')
<ContentEditable
html={editedContents[index]} if (isHidden) return null
onChange={(evt) => handleContentChange(index, evt)}
className="infio-editable-content" return (
/> <div
{(part.added || part.removed) && status === 'active' && ( key={index}
<div className="infio-diff-line-actions"> className={`infio-diff-line ${part.added ? 'added' : part.removed ? 'removed' : ''} ${status !== 'active' ? status : ''}`}
<button >
aria-label="Accept line" <div className="infio-diff-content-wrapper">
onClick={() => acceptDiffLine(index)} <ContentEditable
className="infio-accept" html={editedContents[index]}
> onChange={(evt) => handleContentChange(index, evt)}
{acceptIcon && '✓'} className="infio-editable-content"
</button> />
<button {(part.added || part.removed) && status === 'active' && (
aria-label="Exclude line" <div className="infio-diff-line-actions">
onClick={() => excludeDiffLine(index)} <button
className="infio-exclude" aria-label={t('applyView.acceptLine')}
> onClick={() => acceptDiffLine(index)}
{excludeIcon && '✗'} className="infio-accept"
</button> >
</div> {acceptIcon && '✓'}
)} </button>
</div> <button
</div> aria-label={t('applyView.excludeLine')}
) onClick={() => excludeDiffLine(index)}
})} className="infio-exclude"
</div> >
</div> {excludeIcon && '✗'}
</div> </button>
</div> </div>
</div> )}
<style>{` </div>
</div>
)
})}
</div>
</div>
</div>
</div>
</div>
<style>{`
.infio-diff-content-wrapper { .infio-diff-content-wrapper {
position: relative; position: relative;
width: 100%; width: 100%;
@ -275,6 +273,6 @@ export default function ApplyViewRoot({
opacity: 0.7; opacity: 0.7;
} }
`}</style> `}</style>
</div> </div>
) )
} }

View File

@ -138,5 +138,40 @@ export default {
"updateCommand": "Update Command", "updateCommand": "Update Command",
"errorContentRequired": "Please enter a content for your template", "errorContentRequired": "Please enter a content for your template",
"errorNameRequired": "Please enter a name for your template" "errorNameRequired": "Please enter a name for your template"
},
main: {
openNewChat: "Open new chat",
openInfioCopilot: 'Open infio copilot',
addSelectionToChat: 'Add selection to chat',
rebuildVaultIndex: 'Rebuild entire vault index',
updateVaultIndex: 'Update index for modified files',
autocompleteAccept: 'Autocomplete accept',
autocompletePredict: 'Autocomplete predict',
autocompleteToggle: 'Autocomplete toggle',
autocompleteEnable: 'Autocomplete enable',
autocompleteDisable: 'Autocomplete disable',
inlineEditCommand: 'Inline edit',
},
notifications: {
rebuildingIndex: 'Rebuilding vault index...',
indexingChunks: 'Indexing chunks: {completedChunks} / {totalChunks}',
rebuildComplete: 'Rebuilding vault index complete',
rebuildFailed: 'Rebuilding vault index failed',
updatingIndex: 'Updating vault index...',
updateComplete: 'Vault index updated',
updateFailed: 'Vault index update failed',
selectTextFirst: 'Please select some text first',
migrationFailed: 'Failed to migrate to JSON storage. Please check the console for details.',
reloadingInfio: 'Reloading "infio" due to migration',
},
applyView: {
applyingFile: 'Applying: {{file}}',
acceptChanges: 'Accept changes',
acceptAll: 'Accept All {{shortcut}}',
rejectChanges: 'Reject changes',
rejectAll: 'Reject All {{shortcut}}',
fileNotFound: 'File not found',
acceptLine: 'Accept line',
excludeLine: 'Exclude line',
} }
} }

View File

@ -141,5 +141,40 @@ export default {
"updateCommand": "更新命令", "updateCommand": "更新命令",
"errorContentRequired": "请输入模板内容", "errorContentRequired": "请输入模板内容",
"errorNameRequired": "请输入模板名称" "errorNameRequired": "请输入模板名称"
},
main: {
openNewChat: "打开新聊天",
openInfioCopilot: '打开 Infio Copilot',
addSelectionToChat: '将选定内容添加到聊天',
rebuildVaultIndex: '重建整个 Vault 索引',
updateVaultIndex: '更新已修改文件的索引',
autocompleteAccept: '接受自动完成',
autocompletePredict: '手动触发自动完成',
autocompleteToggle: '切换自动完成',
autocompleteEnable: '启用自动完成',
autocompleteDisable: '禁用自动完成',
inlineEditCommand: '文本内编辑',
},
notifications: {
rebuildingIndex: '正在重建 Vault 索引...',
indexingChunks: '正在索引块:{completedChunks} / {totalChunks}',
rebuildComplete: 'Vault 索引重建完成',
rebuildFailed: 'Vault 索引重建失败',
updatingIndex: '正在更新 Vault 索引...',
updateComplete: 'Vault 索引已更新',
updateFailed: 'Vault 索引更新失败',
selectTextFirst: '请先选择一些文本',
migrationFailed: '迁移到 JSON 存储失败。请检查控制台以获取详细信息。',
reloadingInfio: '因迁移而重新加载 "infio"',
},
applyView: {
applyingFile: '正在应用: {{file}}',
acceptChanges: '接受更改',
acceptAll: '全部接受 {{shortcut}}',
rejectChanges: '拒绝更改',
rejectAll: '全部拒绝 {{shortcut}}',
fileNotFound: '文件未找到',
acceptLine: '接受此行',
excludeLine: '排除此行',
} }
}; };

View File

@ -25,6 +25,7 @@ import RenderSuggestionPlugin from "./render-plugin/render-surgestion-plugin"
import { InlineSuggestionState } from "./render-plugin/states" import { InlineSuggestionState } from "./render-plugin/states"
import { InfioSettingTab } from './settings/SettingTab' import { InfioSettingTab } from './settings/SettingTab'
import StatusBar from "./status-bar" import StatusBar from "./status-bar"
import { t } from './lang/helpers'
import { import {
InfioSettings, InfioSettings,
parseInfioSettings, parseInfioSettings,
@ -32,7 +33,6 @@ import {
import { getMentionableBlockData } from './utils/obsidian' import { getMentionableBlockData } from './utils/obsidian'
import './utils/path' import './utils/path'
export default class InfioPlugin extends Plugin { export default class InfioPlugin extends Plugin {
private metadataCacheUnloadFn: (() => void) | null = null private metadataCacheUnloadFn: (() => void) | null = null
private activeLeafChangeUnloadFn: (() => void) | null = null private activeLeafChangeUnloadFn: (() => void) | null = null
@ -56,7 +56,7 @@ export default class InfioPlugin extends Plugin {
this.addSettingTab(this.settingTab) this.addSettingTab(this.settingTab)
// add icon to ribbon // add icon to ribbon
this.addRibbonIcon('wand-sparkles', 'Open infio copilot', () => this.addRibbonIcon('wand-sparkles', t('main.openInfioCopilot'), () =>
this.openChatView(), this.openChatView(),
) )
@ -163,13 +163,13 @@ export default class InfioPlugin extends Plugin {
/// *** Commands *** /// *** Commands ***
this.addCommand({ this.addCommand({
id: 'open-new-chat', id: 'open-new-chat',
name: 'Open new chat', name: t('main.openNewChat'),
callback: () => this.openChatView(true), callback: () => this.openChatView(true),
}) })
this.addCommand({ this.addCommand({
id: 'add-selection-to-chat', id: 'add-selection-to-chat',
name: 'Add selection to chat', name: t('main.addSelectionToChat'),
editorCallback: (editor: Editor, view: MarkdownView) => { editorCallback: (editor: Editor, view: MarkdownView) => {
this.addSelectionToChat(editor, view) this.addSelectionToChat(editor, view)
}, },
@ -183,9 +183,9 @@ export default class InfioPlugin extends Plugin {
this.addCommand({ this.addCommand({
id: 'rebuild-vault-index', id: 'rebuild-vault-index',
name: 'Rebuild entire vault index', name: t('main.rebuildVaultIndex'),
callback: async () => { callback: async () => {
const notice = new Notice('Rebuilding vault index...', 0) const notice = new Notice(t('notifications.rebuildingIndex'), 0)
try { try {
const ragEngine = await this.getRAGEngine() const ragEngine = await this.getRAGEngine()
await ragEngine.updateVaultIndex( await ragEngine.updateVaultIndex(
@ -195,15 +195,15 @@ export default class InfioPlugin extends Plugin {
const { completedChunks, totalChunks } = const { completedChunks, totalChunks } =
queryProgress.indexProgress queryProgress.indexProgress
notice.setMessage( notice.setMessage(
`Indexing chunks: ${completedChunks} / ${totalChunks}`, t('notifications.indexingChunks', { completedChunks, totalChunks }),
) )
} }
}, },
) )
notice.setMessage('Rebuilding vault index complete') notice.setMessage(t('notifications.rebuildComplete'))
} catch (error) { } catch (error) {
console.error(error) console.error(error)
notice.setMessage('Rebuilding vault index failed') notice.setMessage(t('notifications.rebuildFailed'))
} finally { } finally {
setTimeout(() => { setTimeout(() => {
notice.hide() notice.hide()
@ -214,9 +214,9 @@ export default class InfioPlugin extends Plugin {
this.addCommand({ this.addCommand({
id: 'update-vault-index', id: 'update-vault-index',
name: 'Update index for modified files', name: t('main.updateVaultIndex'),
callback: async () => { callback: async () => {
const notice = new Notice('Updating vault index...', 0) const notice = new Notice(t('notifications.updatingIndex'), 0)
try { try {
const ragEngine = await this.getRAGEngine() const ragEngine = await this.getRAGEngine()
await ragEngine.updateVaultIndex( await ragEngine.updateVaultIndex(
@ -226,15 +226,15 @@ export default class InfioPlugin extends Plugin {
const { completedChunks, totalChunks } = const { completedChunks, totalChunks } =
queryProgress.indexProgress queryProgress.indexProgress
notice.setMessage( notice.setMessage(
`Indexing chunks: ${completedChunks} / ${totalChunks}`, t('notifications.indexingChunks', { completedChunks, totalChunks }),
) )
} }
}, },
) )
notice.setMessage('Vault index updated') notice.setMessage(t('notifications.updateComplete'))
} catch (error) { } catch (error) {
console.error(error) console.error(error)
notice.setMessage('Vault index update failed') notice.setMessage(t('notifications.updateFailed'))
} finally { } finally {
setTimeout(() => { setTimeout(() => {
notice.hide() notice.hide()
@ -245,7 +245,7 @@ export default class InfioPlugin extends Plugin {
this.addCommand({ this.addCommand({
id: 'autocomplete-accept', id: 'autocomplete-accept',
name: 'Autocomplete accept', name: t('main.autocompleteAccept'),
editorCheckCallback: ( editorCheckCallback: (
checking: boolean, checking: boolean,
editor: Editor, editor: Editor,
@ -265,7 +265,7 @@ export default class InfioPlugin extends Plugin {
this.addCommand({ this.addCommand({
id: 'autocomplete-predict', id: 'autocomplete-predict',
name: 'Autocomplete predict', name: t('main.autocompletePredict'),
editorCheckCallback: ( editorCheckCallback: (
checking: boolean, checking: boolean,
editor: Editor, editor: Editor,
@ -288,7 +288,7 @@ export default class InfioPlugin extends Plugin {
this.addCommand({ this.addCommand({
id: "autocomplete-toggle", id: "autocomplete-toggle",
name: "Autocomplete toggle", name: t('main.autocompleteToggle'),
callback: () => { callback: () => {
const newValue = !this.settings.autocompleteEnabled; const newValue = !this.settings.autocompleteEnabled;
this.setSettings({ this.setSettings({
@ -300,7 +300,7 @@ export default class InfioPlugin extends Plugin {
this.addCommand({ this.addCommand({
id: "autocomplete-enable", id: "autocomplete-enable",
name: "Autocomplete enable", name: t('main.autocompleteEnable'),
checkCallback: (checking) => { checkCallback: (checking) => {
if (checking) { if (checking) {
return !this.settings.autocompleteEnabled; return !this.settings.autocompleteEnabled;
@ -316,7 +316,7 @@ export default class InfioPlugin extends Plugin {
this.addCommand({ this.addCommand({
id: "autocomplete-disable", id: "autocomplete-disable",
name: "Autocomplete disable", name: t('main.autocompleteDisable'),
checkCallback: (checking) => { checkCallback: (checking) => {
if (checking) { if (checking) {
return this.settings.autocompleteEnabled; return this.settings.autocompleteEnabled;
@ -332,7 +332,7 @@ export default class InfioPlugin extends Plugin {
this.addCommand({ this.addCommand({
id: "ai-inline-edit", id: "ai-inline-edit",
name: "Inline edit", name: t('main.inlineEditCommand'),
// hotkeys: [ // hotkeys: [
// { // {
// modifiers: ['Mod', 'Shift'], // modifiers: ['Mod', 'Shift'],
@ -342,7 +342,7 @@ export default class InfioPlugin extends Plugin {
editorCallback: (editor: Editor) => { editorCallback: (editor: Editor) => {
const selection = editor.getSelection(); const selection = editor.getSelection();
if (!selection) { if (!selection) {
new Notice("Please select some text first"); new Notice(t('notifications.selectTextFirst'));
return; return;
} }
// Get the selection start position // Get the selection start position
@ -495,7 +495,7 @@ export default class InfioPlugin extends Plugin {
} catch (error) { } catch (error) {
console.error('Failed to migrate to JSON storage:', error) console.error('Failed to migrate to JSON storage:', error)
new Notice( new Notice(
'Failed to migrate to JSON storage. Please check the console for details.', t('notifications.migrationFailed'),
) )
} }
} }
@ -505,7 +505,7 @@ export default class InfioPlugin extends Plugin {
if (leaves.length === 0 || !(leaves[0].view instanceof ChatView)) { if (leaves.length === 0 || !(leaves[0].view instanceof ChatView)) {
return return
} }
new Notice('Reloading "infio" due to migration', 1000) new Notice(t('notifications.reloadingInfio'), 1000)
leaves[0].detach() leaves[0].detach()
await this.activateChatView() await this.activateChatView()
} }